From 5bfad6bf1c6249cb3b5dcfa6baca955bc9b4e1c0 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 18 Oct 2024 16:18:18 +0800 Subject: [PATCH 01/80] fix(self-hosted): GraphiQL invalided authentication credentials (#29972) --- apps/studio/pages/project/[ref]/api/graphiql.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/studio/pages/project/[ref]/api/graphiql.tsx b/apps/studio/pages/project/[ref]/api/graphiql.tsx index a658863ddc5..0d7e0bfb9d8 100644 --- a/apps/studio/pages/project/[ref]/api/graphiql.tsx +++ b/apps/studio/pages/project/[ref]/api/graphiql.tsx @@ -71,7 +71,9 @@ const GraphiQLPage: NextPageWithLayout = () => { ...opts, headers: { ...opts?.headers, - Authorization: `Bearer ${accessToken}`, + ...(accessToken && { + Authorization: `Bearer ${accessToken}`, + }), 'x-graphql-authorization': opts?.headers?.['Authorization'] ?? opts?.headers?.['authorization'] ?? From 9a00bf4354c5a1a9e6cb138ecaa59e80d5382825 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Fri, 18 Oct 2024 10:33:49 +0200 Subject: [PATCH 02/80] fix: update security.txt wording (#29973) Fixup a missed mention of security@supabase.io and point everything to Hackerone. --- apps/docs/public/.well-known/security.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/docs/public/.well-known/security.txt b/apps/docs/public/.well-known/security.txt index 19ee976897f..d73657a4ce2 100644 --- a/apps/docs/public/.well-known/security.txt +++ b/apps/docs/public/.well-known/security.txt @@ -6,7 +6,9 @@ At Supabase, we consider the security of our systems a top priority. But no matt If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems. -Out of scope vulnerabilities: +The full scope of our VDP is available at https://hackerone.com/supabase. + +Here is a brief list of some common out of scope vulnerabilities: - Clickjacking on pages with no sensitive actions. - Unauthenticated/logout/login CSRF. @@ -25,7 +27,7 @@ Testing guidelines: - Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data. Reporting guidelines: -- E-mail your findings to security@supabase.io. +- File a report through our VDP at https://hackerone.com/supabase - Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Disclosure guidelines: @@ -36,7 +38,7 @@ Disclosure guidelines: - information about Supabase employees, contractors or partners What we promise: -- We will respond to your report within 7 business days with our evaluation of the report and an expected resolution date. +- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date. - If you have followed the instructions above, we will not take any legal action against you in regard to the report. - We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission. - We will keep you informed of the progress towards resolving the problem. From d7f92e4d4832d909aca3a259b9ee052abfa48dd8 Mon Sep 17 00:00:00 2001 From: Rodrigo Mansueli Date: Fri, 18 Oct 2024 08:10:01 -0300 Subject: [PATCH 03/80] chore: Update RateLimits.tsx (#28978) * chore: Update RateLimits.tsx Make it clearer * fix: run prettier * fix: apply suggestions from code review Co-authored-by: Alaister Young --------- Co-authored-by: joel Co-authored-by: Alaister Young --- .../components/interfaces/Auth/RateLimits/RateLimits.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index a6258a90e71..5c8c22d1a85 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -408,8 +408,8 @@ const RateLimits = () => { - Number of sign up and sign-in requests that can be made per hour per IP - address (excludes anonymous users). + Number of sign up and sign-in requests that can be made in a 5 minute + interval per IP address (excludes anonymous users).

} > From 9dd7deb990f9da91603bd2e9b23ca67e0d4d8729 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 18 Oct 2024 16:25:13 +0200 Subject: [PATCH 04/80] chore: Add a clean script to all packages/apps (#29926) Add a clean command. --- apps/design-system/package.json | 1 + apps/docs/package.json | 2 +- apps/studio/package.json | 1 + apps/www/package.json | 1 + package-lock.json | 270 ++++++++++++++++++++++++++++-- package.json | 19 +-- packages/ai-commands/package.json | 1 + packages/api-types/package.json | 1 + packages/common/package.json | 1 + packages/generator/package.json | 1 + packages/icons/package.json | 3 +- packages/shared-data/package.json | 4 +- packages/ui-patterns/package.json | 1 + packages/ui/package.json | 1 + playwright-tests/package.json | 3 +- tests/package.json | 1 + turbo.json | 4 + 17 files changed, 279 insertions(+), 36 deletions(-) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 156079b3a62..01cac2a43e7 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -10,6 +10,7 @@ "start": "next start", "lint": "next lint", "content:build": "contentlayer2 build", + "clean": "rimraf node_modules .next .turbo", "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" }, "dependencies": { diff --git a/apps/docs/package.json b/apps/docs/package.json index 1950af77461..62f16690760 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -22,7 +22,7 @@ "last-changed:reset": "npm run last-changed -- --reset", "codegen:references": "tsx features/docs/Reference.generated.script.ts", "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"", - "clean": "rm -rf ./docs/features/docs/generated" + "clean": "rimraf node_modules ./docs/features/docs/generated" }, "dependencies": { "@code-hike/mdx": "^0.9.0", diff --git a/apps/studio/package.json b/apps/studio/package.json index e5afc2f6c2f..0b1f65ed4d8 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -8,6 +8,7 @@ "build": "next build", "start": "next start", "lint": "next lint", + "clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo", "test": "vitest --run", "test:watch": "vitest watch", "test:ui": "vitest --ui", diff --git a/apps/www/package.json b/apps/www/package.json index 6fbfcbd3cd5..48833eec172 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -9,6 +9,7 @@ "export": "next export", "start": "next start", "lint": "next lint", + "clean": "rimraf node_modules", "typecheck": "npm run content:build && tsc --noEmit", "content:build": "contentlayer2 build", "prettier": "prettier --write \"./{pages,components,lib,stores,styles,tests}/**/*.{ts,tsx,md,js,jsx,json}\"", diff --git a/package-lock.json b/package-lock.json index abc76663856..2a1db90666b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "workspaces": [ "apps/*", - "apps/docs/spec/parser", "tests", "playwright-tests", "packages/*" @@ -19,6 +18,7 @@ "eslint": "^8.57.0", "prettier": "^4.0.0-alpha.8", "prettier-plugin-sql-cst": "^0.11.0", + "rimraf": "^6.0.0", "sass": "^1.72.0", "supabase": "^1.151.1", "turbo": "2.0.4" @@ -8206,6 +8206,26 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", @@ -8216,6 +8236,21 @@ "node": ">=10" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { "version": "7.5.4", "license": "ISC", @@ -9680,6 +9715,43 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@octokit/auth-app": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz", @@ -20893,6 +20965,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/del/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -23201,6 +23310,41 @@ "node": ">=12.0.0" } }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", @@ -32791,8 +32935,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "license": "ISC", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -35066,6 +35211,22 @@ "node": ">= 6" } }, + "node_modules/pgsql-parser/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pgsql-parser/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -38767,31 +38928,106 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "license": "ISC", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, "dependencies": { - "glob": "^7.1.3" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/package.json b/package.json index 591bec4f72e..82a14eb6657 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,12 @@ "author": "Supabase, Inc.", "license": "Apache-2.0", "private": true, - "workspaces": [ - "apps/*", - "apps/docs/spec/parser", - "tests", - "playwright-tests", - "packages/*" - ], + "workspaces": ["apps/*", "tests", "playwright-tests", "packages/*"], "scripts": { "build": "turbo run build", "build:studio": "turbo run build --filter=studio", "build:design-system": "turbo run build --filter=design-system", + "clean": "turbo run clean --parallel && rimraf node_modules", "dev": "turbo run dev --parallel", "dev:reference": "turbo run dev --filter=reference --parallel", "dev:studio": "turbo run dev --filter=studio --parallel", @@ -45,6 +40,7 @@ "eslint": "^8.57.0", "prettier": "^4.0.0-alpha.8", "prettier-plugin-sql-cst": "^0.11.0", + "rimraf": "^6.0.0", "sass": "^1.72.0", "supabase": "^1.151.1", "turbo": "2.0.4" @@ -57,13 +53,6 @@ "npm": "^10.0.0", "node": "^20.0.0" }, - "keywords": [ - "postgres", - "firebase", - "storage", - "functions", - "database", - "auth" - ], + "keywords": ["postgres", "firebase", "storage", "functions", "database", "auth"], "packageManager": "npm@10.7.0" } diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index 2db6c03668b..49d84317fc5 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -6,6 +6,7 @@ "license": "MIT", "sideEffects": false, "scripts": { + "clean": "rimraf node_modules", "typecheck": "tsc --noEmit", "test": "jest" }, diff --git a/packages/api-types/package.json b/packages/api-types/package.json index a71266edcba..8225d4d0719 100644 --- a/packages/api-types/package.json +++ b/packages/api-types/package.json @@ -5,6 +5,7 @@ "main": "./index.ts", "types": "./index.ts", "scripts": { + "clean": "rimraf node_modules", "codegen": "openapi-typescript http://localhost:8080/api-json -o types/api.d.ts --alphabetize && prettier --write types/api.d.ts" }, "author": "", diff --git a/packages/common/package.json b/packages/common/package.json index 07194c309d7..34ce7c0c484 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -5,6 +5,7 @@ "types": "./index.tsx", "license": "MIT", "scripts": { + "clean": "rimraf node_modules", "gen:types": "supabase gen types typescript --local >| ./database-types.ts", "typecheck_CURRENTLY_IGNORED": "tsc --noEmit" }, diff --git a/packages/generator/package.json b/packages/generator/package.json index e779983e126..bee0861f1b5 100644 --- a/packages/generator/package.json +++ b/packages/generator/package.json @@ -5,6 +5,7 @@ "types": "./index.tsx", "license": "MIT", "scripts": { + "clean": "rimraf node_modules", "tsdoc:dereference:functions:v1": "tsx ./tsdoc.ts --input ../../apps/docs/spec/enrichments/tsdoc_v1/functions.json --output ../../apps/docs/spec/enrichments/tsdoc_v1/functions_dereferenced.json", "tsdoc:dereference:gotrue:v1": "tsx ./tsdoc.ts --input ../../apps/docs/spec/enrichments/tsdoc_v1/gotrue.json --output ../../apps/docs/spec/enrichments/tsdoc_v1/gotrue_dereferenced.json", "tsdoc:dereference:postgrest:v1": "tsx ./tsdoc.ts --input ../../apps/docs/spec/enrichments/tsdoc_v1/postgrest.json --output ../../apps/docs/spec/enrichments/tsdoc_v1/postgrest_dereferenced.json", diff --git a/packages/icons/package.json b/packages/icons/package.json index a33333c9b24..9db7c532dd8 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -2,7 +2,8 @@ "name": "icons", "main": "./src/icons/index.ts", "scripts": { - "build:icons": "build-icons --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts" + "build:icons": "build-icons --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts", + "clean": "rimraf node_modules" }, "dependencies": { "typescript": "~5.5.0", diff --git a/packages/shared-data/package.json b/packages/shared-data/package.json index 9dd0d49e5be..01d3593e582 100644 --- a/packages/shared-data/package.json +++ b/packages/shared-data/package.json @@ -4,7 +4,9 @@ "description": "Data shared between Supabase apps", "main": "./index.ts", "types": "./index.ts", - "scripts": {}, + "scripts": { + "clean": "rimraf node_modules" + }, "author": "", "license": "MIT" } diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index f1b3bc5d1e3..60589196347 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -6,6 +6,7 @@ "sideEffects": false, "scripts": { "test": "vitest", + "clean": "rimraf node_modules", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/ui/package.json b/packages/ui/package.json index dbbfa23eb6b..df7ad43ac4c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,6 +12,7 @@ "transform-tokens": "node ./transformTokens.js", "extract-design-tokens": "node internals/tokens/extract-design-tokens.js", "generate-styles": "npm run extract-design-tokens && npm run transform-tokens && npm run cleanse-css-for-tailwind && npm run generate-demo-tailwind-classes", + "clean": "rimraf node_modules", "test": "vitest" }, "dependencies": { diff --git a/playwright-tests/package.json b/playwright-tests/package.json index dc978f7caf4..a47f3552792 100644 --- a/playwright-tests/package.json +++ b/playwright-tests/package.json @@ -9,7 +9,8 @@ "test": "playwright test", "posttest": "supabase stop --no-backup", "codegen:setup": "npm run env:setup && NODE_ENV=test npm --prefix ../apps/studio run dev", - "codegen": "playwright codegen http://localhost:8082/project/default" + "codegen": "playwright codegen http://localhost:8082/project/default", + "clean": "rimraf node_modules" }, "keywords": [], "author": "", diff --git a/tests/package.json b/tests/package.json index 79e649014de..9feb6a2070c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -12,6 +12,7 @@ "allure:generate": "rm -rf allure-report && node_modules/allure-commandline/bin/allure generate", "allure:serve": "node_modules/allure-commandline/bin/allure serve", "test:report": "npm run allure:generate && npm run allure:serve", + "clean": "rimraf node_modules", "format": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md}\"" }, "author": "Supabase, Inc.", diff --git a/turbo.json b/turbo.json index 4e4c8327e90..7f58710cd1b 100644 --- a/turbo.json +++ b/turbo.json @@ -77,6 +77,10 @@ "lint": { "outputs": [] }, + "clean": { + "outputs": [], + "cache": false + }, "dev": { "cache": false }, From 784a3aca324bb1ec1a9dd4b05e9d1c664587a822 Mon Sep 17 00:00:00 2001 From: Karan Das <127123023+0xkar4n@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:10:20 +0530 Subject: [PATCH 05/80] Chore: Replaced any with interface CSSProperties (#29650) Chore: Replaced any with interface CSSProperties where css is used as a props --- apps/www/components/Panel/Panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/components/Panel/Panel.tsx b/apps/www/components/Panel/Panel.tsx index 327210fac98..352d9e4e729 100644 --- a/apps/www/components/Panel/Panel.tsx +++ b/apps/www/components/Panel/Panel.tsx @@ -12,8 +12,8 @@ interface Props { hasInnerShimmer?: boolean shimmerFromColor?: string shimmerToColor?: string - style?: any - innerStyle?: any + style?: React.CSSProperties + innerStyle?: React.CSSProperties hasMotion?: boolean } From 995331f912a9a2336d3ed4f9b69cbd55c5281b88 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 18 Oct 2024 15:36:38 -0400 Subject: [PATCH 06/80] Updated the Edge Functions Hero Call to Action text (#29965) Co-authored-by: Greg Papas --- apps/www/data/products/functions/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/data/products/functions/page.tsx b/apps/www/data/products/functions/page.tsx index cf94adb7bca..304be154d07 100644 --- a/apps/www/data/products/functions/page.tsx +++ b/apps/www/data/products/functions/page.tsx @@ -44,7 +44,7 @@ export default (isMobile?: boolean) => ({ image: , ctas: [ { - label: 'Launch a free database', + label: 'Create a free account', href: '/dashboard', type: 'primary' as any, }, From 62dc3ce5144af725ceac7972c8928c00f6b8eeda Mon Sep 17 00:00:00 2001 From: TheOtherBrian1 <91111415+TheOtherBrian1@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:40:48 -0400 Subject: [PATCH 07/80] updated pg_net docs | outlines configuration process (#29984) * updated pg_net docs | outlines configuration process * updated pg_net docs | outlines configuration process --------- Co-authored-by: Brian Brennglass --- .../guides/database/extensions/pg_net.mdx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/guides/database/extensions/pg_net.mdx b/apps/docs/content/guides/database/extensions/pg_net.mdx index 8556179afb4..bfedae9c95d 100644 --- a/apps/docs/content/guides/database/extensions/pg_net.mdx +++ b/apps/docs/content/guides/database/extensions/pg_net.mdx @@ -309,9 +309,9 @@ order by "created" desc; ## Configuration - + -See the [self-hosting docs](/docs/guides/hosting/overview#configuration) for self-hosting instructions. +Supabase supports reconfiguring pg*net starting from v0.10.0+. For the latest release, initiate a postgres upgrade in the [Infrastructure Settings](https://supabase.com/dashboard/project/*/settings/infrastructure). @@ -329,18 +329,11 @@ where "name" like 'pg_net%'; ### Alter settings -Note, that doing `ALTER SYSTEM` requires SUPERUSER but on PostgreSQL >= 15, you can do: +Change variables: ```sql -grant alter system on parameter pg_net.ttl to ; -grant alter system on parameter pg_net.batch_size to ; -``` - -Then variables can be changed: - -```sql -alter system set pg_net.ttl to '1 hour' -alter system set pg_net.batch_size to 500; +alter role "postgres" set pg_net.ttl to '24 hour'; +alter role "postgres" set pg_net.batch_size to 500; ``` Then reload the settings and restart the `pg_net` background worker with: From 9f9b083ba5a31112ec8ceeedeb43ea6fa5958996 Mon Sep 17 00:00:00 2001 From: Thomas <31189692+ecktoteckto@users.noreply.github.com> Date: Sun, 20 Oct 2024 06:29:53 +0200 Subject: [PATCH 08/80] fix: item description and unit price of Billing Breakdown (#29996) --- .../BillingSettings/BillingBreakdown/UpcomingInvoice.tsx | 9 ++++++--- .../interfaces/Organization/BillingSettings/helpers.ts | 3 +++ apps/studio/data/analytics/org-daily-stats-query.ts | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx index f8d9f5c3047..2bfef53e189 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx @@ -113,7 +113,7 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { Usage - + Unit price Cost @@ -124,6 +124,9 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { {item.description ?? 'Unknown'} + {item.usage_metric && + billingMetricUnit(item.usage_metric) && + ` (${billingMetricUnit(item.usage_metric)})`} {!item.proration && ( @@ -131,12 +134,12 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { )} {!item.proration && ( - + {item.unit_price === 0 ? 'FREE' : item.unit_price ? formatCurrency(item.unit_price) - : null} + : `${item.unit_price_desc}`} )} {formatCurrency(item.amount)} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts b/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts index 62677c4ba83..7d89dc35e24 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts @@ -46,6 +46,9 @@ export const billingMetricUnit = (pricingMetric: PricingMetric) => { PricingMetric.PITR_7, PricingMetric.PITR_14, PricingMetric.PITR_28, + PricingMetric.LOG_DRAIN, + PricingMetric.AUTH_MFA_PHONE, + PricingMetric.AUTH_MFA_WEB_AUTHN, ].includes(pricingMetric) ) { return 'Hours' diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts index 4f03e565f34..f6c0ca56ed7 100644 --- a/apps/studio/data/analytics/org-daily-stats-query.ts +++ b/apps/studio/data/analytics/org-daily-stats-query.ts @@ -40,6 +40,8 @@ export enum PricingMetric { DISK_THROUGHPUT_GP3 = 'DISK_THROUGHPUT_GP3', LOG_DRAIN = 'LOG_DRAIN', LOG_DRAIN_EVENTS = 'LOG_DRAIN_EVENTS', + AUTH_MFA_PHONE = 'AUTH_MFA_PHONE', + AUTH_MFA_WEB_AUTHN = 'AUTH_MFA_WEB_AUTHN', } export enum ComputeUsageMetric { From c4dfc4a4547146c4fca55875d6f9c8db013a8ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Mon, 21 Oct 2024 18:22:08 +0800 Subject: [PATCH 09/80] chore: cleanup dead code paths for Stripe (#30001) --- .../CostControl/CostControl.tsx | 39 +------- .../Subscription/Subscription.tsx | 40 +-------- .../Organization/Usage/TotalUsage.tsx | 4 +- .../Organization/Usage/Usage.constants.tsx | 10 +-- .../Addons/ComputeInstanceSidePanel.tsx | 12 --- .../Settings/Addons/CustomDomainSidePanel.tsx | 59 ++---------- .../Settings/Addons/IPv4SidePanel.tsx | 90 +++++-------------- .../Settings/Addons/PITRSidePanel.tsx | 57 ++---------- .../DeployNewReplicaPanel.tsx | 5 +- .../studio/components/ui/Charts/AreaChart.tsx | 2 +- apps/studio/components/ui/Charts/BarChart.tsx | 2 +- ...g-subscription-cancel-schedule-mutation.ts | 52 ----------- .../pages/project/[ref]/reports/database.tsx | 7 +- .../pages/project/[ref]/settings/database.tsx | 9 +- 14 files changed, 46 insertions(+), 342 deletions(-) delete mode 100644 apps/studio/data/subscriptions/org-subscription-cancel-schedule-mutation.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index 172cf511853..0a4e467a763 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -1,6 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import dayjs from 'dayjs' -import { Calendar, ExternalLink } from 'lucide-react' +import { ExternalLink } from 'lucide-react' import { useTheme } from 'next-themes' import Image from 'next/image' import Link from 'next/link' @@ -14,13 +13,12 @@ import { import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationBillingSubscriptionCancelSchedule } from 'data/subscriptions/org-subscription-cancel-schedule-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { BASE_PATH } from 'lib/constants' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' -import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { Alert, Button } from 'ui' import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip' import SpendCapSidePanel from './SpendCapSidePanel' @@ -51,9 +49,6 @@ const CostControl = ({}: CostControlProps) => { const canChangeTier = !projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id || '') - const { mutate: cancelSubscriptionSchedule, isLoading: cancelSubscriptionScheduleLoading } = - useOrganizationBillingSubscriptionCancelSchedule() - return ( <> @@ -132,36 +127,6 @@ const CostControl = ({}: CostControlProps) => {

)} - {/** Toggled on spend cap, scheduled change for end-of-cycle */} - {subscription?.scheduled_plan_change?.target_plan === 'pro' && - subscription?.scheduled_plan_change?.usage_billing_enabled === false && - subscription?.plan.id === 'pro' && ( - - - Scheduled change - -
- Your spend cap will be enabled on{' '} - {dayjs(subscription?.scheduled_plan_change?.at).format('MMMM D, YYYY')}. - You will not be charged for any over-usage moving on. If you would like - to keep the spend cap disabled and scale as you go, you may still cancel - the scheduled change. -
-
- -
-
-
- )} -
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx index 674d32fb95b..c24a6a46c70 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx @@ -1,7 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' -import { Calendar, ExternalLink } from 'lucide-react' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { @@ -13,12 +13,11 @@ import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import SparkBar from 'components/ui/SparkBar' -import { useOrganizationBillingSubscriptionCancelSchedule } from 'data/subscriptions/org-subscription-cancel-schedule-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' -import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { Alert, Button } from 'ui' import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip' import { Restriction } from '../Restriction' import PlanUpdateSidePanel from './PlanUpdateSidePanel' @@ -41,9 +40,6 @@ const Subscription = () => { isSuccess, } = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canReadSubscriptions }) - const { mutate: cancelSubscriptionSchedule, isLoading: cancelSubscriptionScheduleLoading } = - useOrganizationBillingSubscriptionCancelSchedule() - const currentPlan = subscription?.plan const planName = currentPlan?.name ?? 'Unknown' const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc() @@ -99,38 +95,6 @@ const Subscription = () => {

- {subscription?.scheduled_plan_change && - subscription?.scheduled_plan_change?.target_plan !== subscription.plan.id && ( - - - Scheduled downgrade - -
- Your organization will automatically be downgraded from the{' '} - {subscription.plan.name} plan to the{' '} - - {subscription?.scheduled_plan_change?.target_plan} - {' '} - plan on{' '} - {dayjs(subscription?.scheduled_plan_change?.at).format('MMMM D, YYYY')}. - If you would like to stay on the {subscription.plan.name}{' '} - plan, cancel the scheduled downgrade. -
-
- -
-
-
- )} -
diff --git a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx index 04e6b853e75..efc429764c8 100644 --- a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx @@ -215,60 +215,11 @@ const CustomDomainSidePanel = () => {
- {hasChanges && ( - <> - {selectedOption === 'cd_none' || - (selectedCustomDomain?.price ?? 0) < (subscriptionCDOption?.variant.price ?? 0) - ? subscription?.billing_via_partner === false && - // Old addon billing with upfront payment - subscription.usage_based_billing_project_addons === false && ( -

- - Upon clicking confirm, the add-on is removed immediately and any unused time - in the current billing cycle is added as prorated credits to your - organization and used in subsequent billing cycles. - -

- ) - : !subscription?.billing_via_partner && ( -

- {subscription?.usage_based_billing_project_addons === false ? ( - - Upon clicking confirm, the amount of{' '} - - {formatCurrency(selectedCustomDomain?.price)} - {' '} - will be added to your monthly invoice. The addon is prepaid per month and - in case of a downgrade, you get credits for the remaining time. For the - current billing cycle you're immediately charged a prorated amount for the - remaining days. - - ) : ( - - There are no immediate charges. The addon is billed at the end of your - billing cycle based on your usage and prorated to the hour. - - )} -

- )} - - { - // Billed via partner - subscription?.billing_via_partner && - // Project addons are still billed the old way (upfront payment) - subscription?.usage_based_billing_project_addons === false && - // Scheduled billing plan change - subscription.scheduled_plan_change?.target_plan !== undefined && ( - - - - You have a scheduled subscription change that will be canceled if you change - your custom domain add on. - - - ) - } - + {hasChanges && selectedOption !== 'cd_none' && ( +

+ There are no immediate charges. The addon is billed at the end of your billing cycle + based on your usage and prorated to the hour. +

)} {isFreePlan && ( diff --git a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx index ab14efb9640..252cabdfd02 100644 --- a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx @@ -187,72 +187,30 @@ const IPv4SidePanel = () => { {hasChanges && ( <> - {selectedOption === 'ipv4_none' || - (selectedIPv4?.price ?? 0) < (subscriptionIpV4Option?.variant.price ?? 0) ? ( - subscription?.billing_via_partner === false && - // Old addon billing with upfront payment - subscription.usage_based_billing_project_addons === false && ( -

- Upon clicking confirm, the add-on is removed immediately and any unused time in - the current billing cycle is added as prorated credits to your organization and - used in subsequent billing cycles. -

- ) - ) : ( - <> - - There might be some downtime when enabling the add-on since some DNS clients - might have cached the old DNS entry. Generally, this should be less than a - minute. - -

- By default, this is only applied to the Primary database for your project. If{' '} - - Read replicas - {' '} - are used, each replica also gets its own IPv4 address, with a corresponding{' '} - {formatCurrency(selectedIPv4?.price)}{' '} - charge. -

- {!subscription?.billing_via_partner && ( -

- {subscription?.usage_based_billing_project_addons === false ? ( - - Upon clicking confirm, the respective amount will be added to your monthly - invoice. The addon is prepaid per month and in case of a downgrade, you - get credits for the remaining time. For the current billing cycle you're - immediately charged a prorated amount for the remaining days. - - ) : ( - - There are no immediate charges. The addon is billed at the end of your - billing cycle based on your usage and prorated to the hour. - - )} -

- )} - - { - // Billed via partner - subscription?.billing_via_partner && - // Project addons are still billed the old way (upfront payment) - subscription?.usage_based_billing_project_addons === false && - // Scheduled billing plan change - subscription.scheduled_plan_change?.target_plan !== undefined && ( - - - - You have a scheduled subscription change that will be canceled if you - change your PITR add on. - - - ) - } - + + There might be some downtime when enabling the add-on since some DNS clients might + have cached the old DNS entry. Generally, this should be less than a minute. + + {selectedOption !== 'ipv4_none' && ( +

+ By default, this is only applied to the Primary database for your project. If{' '} + + Read replicas + {' '} + are used, each replica also gets its own IPv4 address, with a corresponding{' '} + {formatCurrency(selectedIPv4?.price)}{' '} + charge. +

+ )} + {!subscription?.billing_via_partner && ( +

+ There are no immediate charges. The addon is billed at the end of your billing + cycle based on your usage and prorated to the hour. +

)} )} diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 8169b1ec33f..62b21a77f8f 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -346,58 +346,11 @@ const PITRSidePanel = () => {
)} - {hasChanges && !blockDowngradeDueToReadReplicas && ( - <> - {selectedOption === 'pitr_0' || - (selectedPitr?.price ?? 0) < (subscriptionPitr?.variant.price ?? 0) - ? subscription?.billing_via_partner === false && - // Old addon billing with upfront payment - subscription.usage_based_billing_project_addons === false && ( -

- Upon clicking confirm, the add-on is removed immediately and any unused time - in the current billing cycle is added as prorated credits to your organization - and used in subsequent billing cycles. -

- ) - : !subscription?.billing_via_partner && ( -

- {subscription?.usage_based_billing_project_addons === false ? ( - - Upon clicking confirm, the amount of{' '} - - {formatCurrency(selectedPitr?.price)} - {' '} - will be added to your monthly invoice. The addon is prepaid per month and - in case of a downgrade, you get credits for the remaining time. For the - current billing cycle you're immediately charged a prorated amount for the - remaining days. - - ) : ( - - There are no immediate charges. The addon is billed at the end of your - billing cycle based on your usage and prorated to the hour. - - )} -

- )} - - { - // Billed via partner - subscription?.billing_via_partner && - // Project addons are still billed the old way (upfront payment) - subscription?.usage_based_billing_project_addons === false && - // Scheduled billing plan change - subscription.scheduled_plan_change?.target_plan !== undefined && ( - - - - You have a scheduled subscription change that will be canceled if you change - your PITR add on. - - - ) - } - + {hasChanges && !blockDowngradeDueToReadReplicas && selectedOption !== 'pitr_0' && ( +

+ There are no immediate charges. The addon is billed at the end of your billing cycle + based on your usage and prorated to the hour. +

)} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index a1e5ab6d240..7a6bcf26f45 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -98,10 +98,7 @@ const DeployNewReplicaPanel = ({ // @ts-ignore const { size_gb, type, throughput_mbps, iops } = diskConfiguration?.attributes ?? {} - const showNewDiskManagementUI = - diskManagementV2 && - subscription?.usage_based_billing_project_addons && - project?.cloud_provider === 'AWS' + const showNewDiskManagementUI = diskManagementV2 && project?.cloud_provider === 'AWS' const readReplicaDiskSizes = (size_gb ?? 0) * 1.25 const additionalCostDiskSize = readReplicaDiskSizes * DISK_PRICING[type as DiskType]?.storage ?? 0 const additionalCostIOPS = calculateIOPSPrice({ diff --git a/apps/studio/components/ui/Charts/AreaChart.tsx b/apps/studio/components/ui/Charts/AreaChart.tsx index 933999a0cf1..c5820d5b7e9 100644 --- a/apps/studio/components/ui/Charts/AreaChart.tsx +++ b/apps/studio/components/ui/Charts/AreaChart.tsx @@ -48,7 +48,7 @@ const AreaChart = ({ if (data.length === 0) { return ( , - 'mutationFn' -> = {}) => { - const queryClient = useQueryClient() - - return useMutation( - (vars) => cancelSubscriptionSchedule(vars), - { - async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(subscriptionKeys.orgSubscription(variables.slug)) - await onSuccess?.(data, variables, context) - }, - async onError(data, variables, context) { - if (onError === undefined) { - toast.error(`Failed to cancel subscription schedule: ${data.message}`) - } else { - onError(data, variables, context) - } - }, - ...options, - } - ) -} diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 5e565ec1dc8..c573b79a3c2 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -26,7 +26,6 @@ import { TIME_PERIODS_INFRA } from 'lib/constants/metrics' import { formatBytes } from 'lib/helpers' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import type { NextPageWithLayout } from 'types' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' @@ -53,11 +52,7 @@ const DatabaseUsage = () => { const isReplicaSelected = state.selectedDatabaseId !== project?.ref - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) - const showNewDiskManagementUI = - subscription?.usage_based_billing_project_addons && - diskManagementV2 && - project?.cloud_provider === 'AWS' + const showNewDiskManagementUI = diskManagementV2 && project?.cloud_provider === 'AWS' const report = useDatabaseReport() const { data } = useDatabaseSizeQuery({ diff --git a/apps/studio/pages/project/[ref]/settings/database.tsx b/apps/studio/pages/project/[ref]/settings/database.tsx index 2a7b0c0239c..23d237dfb53 100644 --- a/apps/studio/pages/project/[ref]/settings/database.tsx +++ b/apps/studio/pages/project/[ref]/settings/database.tsx @@ -15,20 +15,13 @@ import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfigu import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' import DiskSizeConfiguration from 'components/interfaces/Settings/Database/DiskSizeConfiguration' import { useFlag } from 'hooks/ui/useFlag' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' const ProjectSettings: NextPageWithLayout = () => { const diskManagementV2 = useFlag('diskManagementV2') const project = useSelectedProject() - const selectedOrg = useSelectedOrganization() - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) - const showNewDiskManagementUI = - diskManagementV2 && - project?.cloud_provider === 'AWS' && - subscription?.usage_based_billing_project_addons + const showNewDiskManagementUI = diskManagementV2 && project?.cloud_provider === 'AWS' return ( <> From 442bba9ed58b8ab02525188f85ef2ed7e672baf0 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:44:42 +0200 Subject: [PATCH 10/80] fix copy btn alignment, fix titles (#29980) --- .../DatabaseApiSelectionRender.tsx | 10 ++++++---- .../FunctionInvocationSelectionRender.tsx | 10 ++++++---- .../interfaces/Settings/Logs/LogsFormatters.tsx | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx index c7a6d94f3f1..055552e3e7a 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx @@ -39,8 +39,8 @@ const DatabaseApiSelectionRender = ({ log }: any) => {
-

Request Metadata

-
+        

Request Metadata

+
           
{
-

Response Metadata

-
+        

+ Response Metadata +

+
           
)}
-

Request Metadata

-
+        

Request Metadata

+
           
-

Response Metadata

-
+        

+ Response Metadata +

+
           
{ return ( -
+
{label} {valueRender ?? value} From b319ea91a45c86ac186e87604547c277d962adf3 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 21 Oct 2024 21:14:46 +0900 Subject: [PATCH 11/80] docs: Update auth self-host docs to the latest info (#29994) * docs: Update auth self-host docs to the latest info * update description of URI_ALLOW_LIST * update the variable name * Add more description about how to configure auth on self-hosted environment * minor styling fix --- .../self-hosting/analytics/config/page.tsx | 2 +- .../self-hosting/auth/config/page.tsx | 2 +- .../self-hosting/realtime/config/page.tsx | 2 +- .../self-hosting/storage/config/page.tsx | 2 +- .../content/guides/self-hosting/docker.mdx | 2 +- apps/docs/spec/gotrue_v1_config.yaml | 60 +++++++++++-------- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx index d397c0003f0..03a88cc171f 100644 --- a/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx +++ b/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx @@ -21,7 +21,7 @@ const AnalyticsConfigPage = async () => { diff --git a/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx index d8a33d04694..9d6ff028b51 100644 --- a/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx +++ b/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx @@ -21,7 +21,7 @@ const AuthConfigPage = async () => { diff --git a/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx index 01180ba859c..d28ca1077a7 100644 --- a/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx +++ b/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx @@ -21,7 +21,7 @@ const RealtimeConfigPage = async () => { diff --git a/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx index 766bc4d190a..1201543f943 100644 --- a/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx +++ b/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx @@ -21,7 +21,7 @@ const StorageConfigPage = async () => { diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index d45e1886573..4139801d85b 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -265,7 +265,7 @@ Each system has a number of configuration options which can be found in the rele - [Postgres](https://hub.docker.com/_/postgres/) - [PostgREST](https://postgrest.org/en/stable/configuration.html) - [Realtime](https://github.com/supabase/realtime#server) -- [GoTrue](https://github.com/supabase/gotrue) +- [Auth](https://github.com/supabase/auth) - [Storage](https://github.com/supabase/storage-api) - [Kong](https://docs.konghq.com/gateway/latest/install/docker/) - [Supavisor](https://supabase.github.io/supavisor/development/docs/) diff --git a/apps/docs/spec/gotrue_v1_config.yaml b/apps/docs/spec/gotrue_v1_config.yaml index 31827802154..ff4a418c9ff 100644 --- a/apps/docs/spec/gotrue_v1_config.yaml +++ b/apps/docs/spec/gotrue_v1_config.yaml @@ -11,9 +11,11 @@ info: title: 'GoTrue' # {string} A readable name. source: 'https://github.com/supabase/gotrue' # {string} Where developers can find the source code. bugs: 'https://github.com/supabase/gotrue/issues' # {string} Where developers can file bugs. - spec: 'https://github.com/supabase/supabase/blob/master/spec/gotrue_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs). + spec: 'https://github.com/supabase/supabase/blob/master/docs/spec/gotrue_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs). description: | - A `config.toml` file is generated after running `supabase init`. This file is located in the `supabase` folder under `supabase/config.toml`. + In a self-hosted environment, you do not have access to the Auth configuration such as third party OAuth provider settings through the Supabase dashboard. Instead, you configure them through the `docker-compose.yml` file. You can read more about the configuration in the [Self-hosting guide](/docs/guides/self-hosting/docker#configuring-services). + + You can find the complete list of available configuration parameters on the README of the [Auth repository](https://github.com/supabase/auth?tab=readme-ov-file#configuration). tags: - id: general title: General @@ -21,58 +23,68 @@ info: # This section is an array of public functions which a user might need to execute. parameters: - - id: 'project_id' # {string} A unique identifier for this param. - title: 'project_id' # {string} Any name. - tags: ['general'] # {string[]} These tags are useful for grouping parameters - required: true - # default: '5432' - description: | - A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`. - - id: 'GOTRUE_EXTERNAL_GITHUB' # {string} A unique identifier for this param. - title: 'auth.external.github' # {string} Any name. - tags: ['general'] # {string[]} These tags are useful for grouping parameters - required: true - # default: '5432' - description: | - Describes whether the Github provider is enabled or not. - id: 'GOTRUE_SITE_URL' # {string} A unique identifier for this param. - title: 'auth.site_url' # {string} Any name. + title: 'GOTRUE_SITE_URL' # {string} Any name. tags: ['general'] # {string[]} These tags are useful for grouping parameters required: true description: | The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails. + - id: 'GOTRUE_EXTERNAL_GITHUB_ENABLED' # {string} A unique identifier for this param. + title: 'GOTRUE_EXTERNAL_GITHUB_ENABLED' # {string} Any name. + tags: ['general'] # {string[]} These tags are useful for grouping parameters + required: false + description: | + Whether the external provider, GitHub in this case, is enabled or not. + - id: 'GOTRUE_EXTERNAL_GITHUB_CLIENT_ID' # {string} A unique identifier for this param. + title: 'GOTRUE_EXTERNAL_GITHUB_CLIENT_ID' # {string} Any name. + tags: ['general'] # {string[]} These tags are useful for grouping parameters + required: false + description: | + The OAuth2 Client ID registered with the external provider. + - id: 'GOTRUE_EXTERNAL_GITHUB_SECRET' # {string} A unique identifier for this param. + title: 'GOTRUE_EXTERNAL_GITHUB_SECRET' # {string} Any name. + tags: ['general'] # {string[]} These tags are useful for grouping parameters + required: false + description: | + The OAuth2 Client Secret provided by the external provider when you registered. + - id: 'GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI' # {string} A unique identifier for this param. + title: 'GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI' # {string} Any name. + tags: ['general'] # {string[]} These tags are useful for grouping parameters + required: false + description: | + The URI a OAuth2 provider will redirect to with the `code` and `state` values. - id: 'GOTRUE_URI_ALLOW_LIST' # {string} A unique identifier for this param. - title: 'auth.additional_redirect_urls' # {string} Any name. + title: 'GOTRUE_URI_ALLOW_LIST' # {string} Any name. tags: ['general'] # {string[]} These tags are useful for grouping parameters required: true description: | - A list of *exact* URLs that auth providers are permitted to redirect to post authentication. + A comma separated list of URIs (e.g. `"https://foo.example.com,https://*.foo.example.com,https://bar.example.com"`) which are permitted as valid `redirect_to` destinations. - id: 'GOTRUE_JWT_EXP' # {string} A unique identifier for this param. - title: 'auth.jwt_expiry' # {string} Any name. + title: 'GOTRUE_JWT_EXP' # {string} Any name. tags: ['general'] # {string[]} These tags are useful for grouping parameters required: true description: | How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week). - id: 'GOTRUE_DISABLE_SIGNUP' # {string} A unique identifier for this param. - title: 'auth.enable_signup' # {string} Any name. + title: 'GOTRUE_DISABLE_SIGNUP' # {string} Any name. tags: ['general'] # {string[]} These tags are useful for grouping parameters required: true description: | Allow/disallow new user signups to your project. - id: 'GOTRUE_EXTERNAL_EMAIL_ENABLED' # {string} A unique identifier for this param. - title: 'auth.email.enable_signup' # {string} Any name. + title: 'GOTRUE_EXTERNAL_EMAIL_ENABLED' # {string} Any name. tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters required: true description: | Allow/disallow new user signups via email to your project. - id: 'GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED' # {string} A unique identifier for this param. - title: 'auth.email.double_confirm_changes' # {string} Any name. + title: 'GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED' # {string} Any name. tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters required: true description: | If enabled, a user will be required to confirm any email change on both the old, and new email addresses. If disabled, only the new email is required to confirm. - id: 'GOTRUE_MAILER_AUTOCONFIRM' # {string} A unique identifier for this param. - title: 'auth.email.enable_confirmations' # {string} Any name. + title: 'GOTRUE_MAILER_AUTOCONFIRM' # {string} Any name. tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters required: true description: | From 262c4393457893bbacd8bd6ce2b6789c7ff08749 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Mon, 21 Oct 2024 10:22:02 -0230 Subject: [PATCH 12/80] Pass search term to entity query (#29986) --- .../Database/Indexes/CreateIndexSidePanel.tsx | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index dd527130d09..9faecac311e 100644 --- a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -1,4 +1,4 @@ -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check, ChevronsUpDown, Loader2 } from 'lucide-react' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' @@ -50,7 +50,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = const [selectedIndexType, setSelectedIndexType] = useState(INDEX_TYPES[0].value) const [schemaDropdownOpen, setSchemaDropdownOpen] = useState(false) const [tableDropdownOpen, setTableDropdownOpen] = useState(false) - + const [searchTerm, setSearchTerm] = useState('') const { refetch: refetchIndexes } = useIndexesQuery({ schema: selectedSchema, projectRef: project?.ref, @@ -60,10 +60,10 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = projectRef: project?.ref, connectionString: project?.connectionString, }) - const { data: entities, isLoading } = useEntityTypesQuery({ + const { data: entities, isLoading: isLoadingEntities } = useEntityTypesQuery({ schema: selectedSchema, sort: 'alphabetical', - search: undefined, + search: searchTerm, projectRef: project?.ref, connectionString: project?.connectionString, }) @@ -94,6 +94,10 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = [entities?.pages] ) + function handleSearchChange(value: string) { + setSearchTerm(value) + } + const columns = tableColumns?.result[0]?.columns ?? [] const columnOptions: MultiSelectOption[] = columns .filter((column): column is NonNullable => column !== null) @@ -181,7 +185,11 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp sameWidthAsTrigger > - + No schemas found @@ -218,7 +226,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp name="select-table" description={ isSelectEntityDisabled && - !isLoading && + !isLoadingEntities && 'Create a table in this schema via the Table or SQL editor first' } isReactForm={false} @@ -228,28 +236,27 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp open={tableDropdownOpen} onOpenChange={setTableDropdownOpen} > - - {isLoading ? ( - - ) : ( - - )} + + - - + {/* [Terry] shouldFilter context: + https://github.com/pacocoursey/cmdk/issues/267#issuecomment-2252717107 */} + + - No tables found + + {isLoadingEntities ? ( +
+ + Loading... +
+ ) : ( + 'No tables found' + )} +
7 ? 'h-[210px]' : ''}> {(entityTypes ?? []).map((entity) => ( From 2e159931e1fccd6f0ecbd050c461d6f7aa8a0c76 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:21:24 +0200 Subject: [PATCH 13/80] fix crash in settings page (#30018) * fix * fix possible crash on authorize page * Fix formatting * fix type err --------- Co-authored-by: Terry Sutton --- .../DiskManagementPanelForm.tsx | 22 +++++++++++++------ apps/studio/pages/authorize.tsx | 10 ++++----- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx index 98221dbad77..5e58cd95843 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx @@ -123,7 +123,7 @@ export function DiskManagementPanelForm() { const { data: addons } = useProjectAddonsQuery({ projectRef }) const currentCompute = (addons?.selected_addons ?? []).find( - (x) => x.type === 'compute_instance' + (x: { type: string }) => x.type === 'compute_instance' )?.variant const maxIopsBasedOnCompute = COMPUTE_SIZE_MAX_IOPS[(currentCompute?.identifier ?? '') as keyof typeof COMPUTE_SIZE_MAX_IOPS] @@ -135,7 +135,7 @@ export function DiskManagementPanelForm() { const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) - const planId = subscription?.plan.id ?? '' + const planId = subscription?.plan.id ?? 'free' const isPlanUpgradeRequired = subscription?.plan.id === 'pro' && !subscription.usage_billing_enabled @@ -156,7 +156,7 @@ export function DiskManagementPanelForm() { }) const defaultValues = { - storageType: type, + storageType: type || DiskType.GP3, provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, @@ -180,8 +180,7 @@ export function DiskManagementPanelForm() { isWithinCooldownWindow || !canUpdateDiskConfiguration - const { includedDiskGB: includedDiskGBMeta } = - PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {} + const { includedDiskGB: includedDiskGBMeta } = PLAN_DETAILS[planId] const includedDiskGB = includedDiskGBMeta[watchedStorageType] const minIOPS = IOPS_RANGE[watchedStorageType]?.min ?? 0 @@ -201,12 +200,21 @@ export function DiskManagementPanelForm() { updateDiskConfigurationRQ({ ref: projectRef, ...data }) } + // i hate typescript enums + // gotta clean this up but it allows me to remove type casts for now + function getEnumFromFormValue(value: string) { + if (value === 'io2') return DiskType.IO2 + if (value === 'gp3') return DiskType.GP3 + return DiskType.IO2 + } + + const newStorageType = form.getValues('storageType') const diskSizePrice = calculateDiskSizePrice({ planId, oldSize: form.formState.defaultValues?.totalSize || 0, - oldStorageType: form.formState.defaultValues?.storageType as DiskType, + oldStorageType: getEnumFromFormValue(form.formState.defaultValues?.storageType || 'io2'), newSize: form.getValues('totalSize'), - newStorageType: form.getValues('storageType') as DiskType, + newStorageType: getEnumFromFormValue(newStorageType), }) const iopsPrice = calculateIOPSPrice({ diff --git a/apps/studio/pages/authorize.tsx b/apps/studio/pages/authorize.tsx index 2c23397a595..f79e8d7a2d3 100644 --- a/apps/studio/pages/authorize.tsx +++ b/apps/studio/pages/authorize.tsx @@ -44,7 +44,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { useEffect(() => { if (!isLoadingOrganizations) { - setSelectedOrgSlug(organizations?.[0].slug ?? undefined) + setSelectedOrgSlug(organizations?.[0]?.slug ?? undefined) } }, [isLoadingOrganizations]) @@ -113,7 +113,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { if (isApproved) { const approvedOrganization = organizations?.find( - (org) => org.slug === requester.approved_organization_slug + (org) => org?.slug === requester.approved_organization_slug ) return ( @@ -184,9 +184,9 @@ const APIAuthorizationPage: NextPageWithLayout = () => { > {(organizations ?? []).map((organization) => ( {organization.name} From e3e6f7dcf75fa90dae9c7c51d3e5c4943fd05f78 Mon Sep 17 00:00:00 2001 From: Mark Burggraf Date: Mon, 21 Oct 2024 17:22:43 -0700 Subject: [PATCH 14/80] =?UTF-8?q?added=20vercel-postgres=20migration=20gui?= =?UTF-8?q?de=20'Migrate=20from=20Vercel=20Postgres=20t=E2=80=A6=20(#29983?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added vercel-postgres migration guide 'Migrate from Vercel Postgres to Supabase' * fix: icons and build Update apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- apps/docs/app/page.tsx | 63 +----------- .../NavigationMenu.constants.ts | 65 +++++++++++-- .../guides/platform/migrating-to-supabase.mdx | 8 +- .../migrating-to-supabase/vercel-postgres.mdx | 97 +++++++++++++++++++ .../public/img/icons/vercel-icon-light.svg | 3 + apps/docs/public/img/icons/vercel-icon.svg | 3 + 6 files changed, 170 insertions(+), 69 deletions(-) create mode 100644 apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx create mode 100644 apps/docs/public/img/icons/vercel-icon-light.svg create mode 100644 apps/docs/public/img/icons/vercel-icon.svg diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index ad7a21f787b..b01885c6b6d 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -4,6 +4,7 @@ import { IconBackground, TextLink } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker' +import { MIGRATION_PAGES } from '~/components/Navigation/NavigationMenu/NavigationMenu.constants' import { GlassPanelWithIconPicker } from '~/features/ui/GlassPanelWithIconPicker' import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker' import HomeLayout from '~/layouts/HomeLayout' @@ -70,60 +71,6 @@ const products = [ }, ] -const migrationGuides = [ - { - title: 'Auth0', - icon: '/docs/img/icons/auth0-icon', - href: '/guides/resources/migrating-to-supabase/auth0', - hasLightIcon: true, - }, - { - title: 'Firebase Auth', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firebase-auth', - }, - { - title: 'Firestore Data', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firestore-data', - }, - { - title: 'Firebase Storage', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firebase-storage', - }, - { - title: 'Heroku', - icon: '/docs/img/icons/heroku-icon', - href: '/guides/resources/migrating-to-supabase/heroku', - }, - { - title: 'Render', - icon: '/docs/img/icons/render-icon', - href: '/guides/resources/migrating-to-supabase/render', - }, - { - title: 'Amazon RDS', - icon: '/docs/img/icons/aws-rds-icon', - href: '/guides/resources/migrating-to-supabase/amazon-rds', - }, - { - title: 'Postgres', - icon: '/docs/img/icons/postgres-icon', - href: '/guides/resources/migrating-to-supabase/postgres', - }, - { - title: 'MySQL', - icon: '/docs/img/icons/mysql-icon', - href: '/guides/resources/migrating-to-supabase/mysql', - }, - { - title: 'MSSQL', - icon: '/docs/img/icons/mssql-icon', - href: '/guides/resources/migrating-to-supabase/mssql', - }, -] - const selfHostingOptions = [ { title: 'Auth', @@ -268,11 +215,11 @@ const HomePage = () => (
    - {migrationGuides.map((guide) => { + {MIGRATION_PAGES.map((guide) => { return ( -
  • - - +
  • + +
  • ) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index ac355889d2d..6aa8637e1ff 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1,3 +1,7 @@ +import type { ComponentProps } from 'react' + +import type { IconPanel } from 'ui-patterns/IconPanel' + import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types' export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ @@ -1809,23 +1813,64 @@ export const local_development: NavMenuConstant = { ], } -export const MIGRATION_PAGES: Partial[] = [ - { name: 'Amazon RDS', url: '/guides/platform/migrating-to-supabase/amazon-rds' }, - { name: 'Auth0', url: '/guides/platform/migrating-to-supabase/auth0' }, - { name: 'Firebase Auth', url: '/guides/platform/migrating-to-supabase/firebase-auth' }, +export const MIGRATION_PAGES: Partial>[] = [ { - name: 'Firebase Firestore', + name: 'Auth0', + icon: '/docs/img/icons/auth0-icon', + url: '/guides/platform/migrating-to-supabase/auth0', + hasLightIcon: true, + }, + { + name: 'Firebase Auth', + icon: '/docs/img/icons/firebase-icon', + url: '/guides/platform/migrating-to-supabase/firebase-auth', + }, + { + name: 'Firestore Data', + icon: '/docs/img/icons/firebase-icon', url: '/guides/platform/migrating-to-supabase/firestore-data', }, { name: 'Firebase Storage', + icon: '/docs/img/icons/firebase-icon', url: '/guides/platform/migrating-to-supabase/firebase-storage', }, - { name: 'Heroku Postgres', url: '/guides/platform/migrating-to-supabase/heroku' }, - { name: 'MySQL', url: '/guides/platform/migrating-to-supabase/mysql' }, - { name: 'MSSQL', url: '/guides/platform/migrating-to-supabase/mssql' }, - { name: 'Postgres', url: '/guides/platform/migrating-to-supabase/postgres' }, - { name: 'Render', url: '/guides/platform/migrating-to-supabase/render' }, + { + name: 'Heroku', + icon: '/docs/img/icons/heroku-icon', + url: '/guides/platform/migrating-to-supabase/heroku', + }, + { + name: 'Render', + icon: '/docs/img/icons/render-icon', + url: '/guides/platform/migrating-to-supabase/render', + }, + { + name: 'Amazon RDS', + icon: '/docs/img/icons/aws-rds-icon', + url: '/guides/platform/migrating-to-supabase/amazon-rds', + }, + { + name: 'Postgres', + icon: '/docs/img/icons/postgres-icon', + url: '/guides/platform/migrating-to-supabase/postgres', + }, + { + name: 'Vercel Postgres', + icon: '/docs/img/icons/vercel-icon', + url: '/guides/platform/migrating-to-supabase/vercel-postgres', + hasLightIcon: true, + }, + { + name: 'MySQL', + icon: '/docs/img/icons/mysql-icon', + url: '/guides/platform/migrating-to-supabase/mysql', + }, + { + name: 'MSSQL', + icon: '/docs/img/icons/mssql-icon', + url: '/guides/platform/migrating-to-supabase/mssql', + }, ] export const platform: NavMenuConstant = { diff --git a/apps/docs/content/guides/platform/migrating-to-supabase.mdx b/apps/docs/content/guides/platform/migrating-to-supabase.mdx index 494520af8af..f4715f672cf 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase.mdx @@ -11,7 +11,13 @@ Learn how to migrate to Supabase from another database service.
    {migrationPages.map((page) => ( - + ))}
    diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx new file mode 100644 index 00000000000..a94bfaf697f --- /dev/null +++ b/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx @@ -0,0 +1,97 @@ +--- +title: 'Migrate from Vercel Postgres to Supabase' +description: 'Migrate your existing Vercel Postgres database to Supabase.' +subtitle: 'Migrate your existing Vercel Postgres database to Supabase.' +--- + +This guide demonstrates how to migrate your Vercel Postgres database to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project. + +## Retrieve your Vercel Postgres database credentials [#retrieve-credentials] + +1. Log in to your Vercel Dashboard [https://vercel.com/login](https://vercel.com/login). +1. Click on the **Storage** tab. +1. Click on your Postgres Database. +1. Under the **Quickstart** section, select **psql** then click **Show Secret** to reveal your database password. +1. Copy the string after `psql ` to the clipboard. + +Example: + +```bash +psql "postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require" +``` + +Copy this part to your clipboard: + +```bash +"postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require" +``` + +## Set your OLD_DB_URL environment variable + +Set the **OLD_DB_URL** environment variable at the command line using your Vercel Postgres Database credentials. + +Example: + +```bash +export OLD_DB_URL="postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require" +``` + +## Retrieve your Supabase connection string [#retrieve-supabase-connection-string] + +1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). + Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). + +1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. +1. Under **Connection string**, select **URI**, make sure **Display connection pooler** is checked, and **Mode: Session** is set. +1. Click the **Copy** button to the right of your connection string to copy it to the clipboard. + +## Set your NEW_DB_URL environment variable + +Set the **NEW_DB_URL** environment variable at the command line using your Supabase connection string. You will need to replace `[YOUR-PASSWORD]` with your actual database password. + +Example: + +```bash +export NEW_DB_URL="postgresql://postgres.xxxxxxxxxxxxxxxxxxxx:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres" +``` + +## Migrate the database + +You will need the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full [PostgreSQL installation](https://www.postgresql.org/download). + +1. Export your database to a file in console + + Use `pg_dump` with your Postgres credentials to export your database to a file (e.g., `dump.sql`). + +```bash +pg_dump "$OLD_DB_URL" \ + --clean \ + --if-exists \ + --quote-all-identifiers \ + --no-owner \ + --no-privileges \ + > dump.sql +``` + +2. Import the database to your Supabase project + + Use `psql` to import the Postgres database file to your Supabase project. + + ```bash + psql -d "$NEW_DB_URL" -f dump.sql + ``` + +Additional options + +- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command. +- To exclude a schema: `--exclude-schema=PATTERN`. +- To only migrate a single table: `--table=PATTERN`. +- To exclude a table: `--exclude-table=PATTERN`. + +Run `pg_dump --help` for a full list of options. + + + +## Enterprise + +[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project. diff --git a/apps/docs/public/img/icons/vercel-icon-light.svg b/apps/docs/public/img/icons/vercel-icon-light.svg new file mode 100644 index 00000000000..72948d01a7d --- /dev/null +++ b/apps/docs/public/img/icons/vercel-icon-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/docs/public/img/icons/vercel-icon.svg b/apps/docs/public/img/icons/vercel-icon.svg new file mode 100644 index 00000000000..4e3b0e93358 --- /dev/null +++ b/apps/docs/public/img/icons/vercel-icon.svg @@ -0,0 +1,3 @@ + + + From 8271fa3c51c172c296e69f58db1ab112f2113344 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:03:10 +0300 Subject: [PATCH 15/80] Docs: add --no-restart flag to Postgres Config doc (#30019) Update custom-postgres-config.mdx --- apps/docs/content/guides/database/custom-postgres-config.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index ba8093c1c4e..1fb50e31c2e 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -160,6 +160,6 @@ alter role "" set "" to default; ### Considerations -1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds. +1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you’re making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won’t see those changes take effect until a restart is manually triggered. 1. Custom Postgres Config will always override the default optimizations generated by Supabase. When changing compute add-ons, you should also review and update your custom Postgres Config to ensure they remain compatible and effective with the updated compute. 1. Some parameters (e.g. `wal_keep_size`) can increase disk utilization, triggering disk expansion, which in turn can lead to [increases in your bill](/docs/guides/platform/compute-add-ons#disk-io). From 1dcc16ea41c6e764041ab63e4807e2c3b4f4f61e Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:03:36 +0200 Subject: [PATCH 16/80] chore: update cli reference doc (#29740) --- apps/docs/spec/cli_v1_commands.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index cda35983ec8..d207ef08c80 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.200.3 + version: 1.207.8 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -1128,6 +1128,10 @@ commands: name: --config description: Config overrides specified as a 'key=value' pair default_value: '[]' + - id: no-restart + name: --no-restart + description: Do not restart the database after updating config. + default_value: 'false' - id: replace-existing-overrides name: --replace-existing-overrides description: | @@ -2878,7 +2882,7 @@ commands: Requires the local development stack to be started by running `supabase start`. - Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql` by default, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded. + Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql`, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded. Note that since Postgres roles are cluster level entities, those changes will persist between resets. In order to reset custom roles, you need to restart the local development stack. examples: @@ -2976,7 +2980,7 @@ commands: default_value: 'false' - id: include-seed name: --include-seed - description: Include seed data from supabase/seed.sql. + description: Include seed data from your config. default_value: 'false' - id: linked name: --linked From d586346e275b20da133f2439c883ddff73996b89 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Oct 2024 15:21:06 +0800 Subject: [PATCH 17/80] Chore/sentry fixes 211024 (#30003) * Resolve https://supabase.sentry.io/issues/5808689343 * Fix SecurityStatus map key prop issue * Fix FileExplorerHeader to use ButtonTooltip * Fix https://supabase.sentry.io/issues/5998687405 * Fix https://supabase.sentry.io/issues/5414129652 * Fix https://supabase.sentry.io/issues/5414129652 * Fix map key issue in QueryPerformanceGrid * Add whitelist error for sign up mutation * Fix map key in DiskUsage * Fix prop name in info-tooltip --- .../grid/components/editor/TextEditor.tsx | 8 +- .../interfaces/Auth/Users/Users.utils.tsx | 7 +- .../interfaces/Home/SecurityStatus.tsx | 11 +-- .../Usage/UsageSection/DiskUsage.tsx | 5 +- .../QueryPerformance/QueryPerformanceGrid.tsx | 1 + .../StorageExplorer/ColumnContextMenu.tsx | 16 ++-- .../StorageExplorer/FileExplorerHeader.tsx | 88 ++++++------------- .../retrieve-index-from-select-query.ts | 1 + apps/studio/data/misc/signup-mutation.ts | 1 + apps/studio/pages/authorize.tsx | 13 ++- packages/ui-patterns/info-tooltip.tsx | 4 +- 11 files changed, 68 insertions(+), 87 deletions(-) diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index 7496f78c727..14c111adea1 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -15,6 +15,7 @@ import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_, + cn, } from 'ui' import { useTrackedState } from '../../store/Store' import { BlockKeys } from '../common/BlockKeys' @@ -188,9 +189,10 @@ export const TextEditor = ({ } >
    setIsPopoverOpen(!isPopoverOpen)} > {value === null ? : value === '' ? : value} diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index 54ace3d0262..4bb4eaf5024 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -210,9 +210,12 @@ export function getAvatarUrl(user: User): string | undefined { profile_url || profileImageUrl || profileImageURL || - profile_image_url) as string | undefined + profile_image_url || + '') as string | undefined - return SUPPORTED_CSP_AVATAR_URLS.some((x) => (url ?? '').startsWith(x)) ? url : undefined + if (url === undefined) return undefined + const isSupported = SUPPORTED_CSP_AVATAR_URLS.some((x) => url.startsWith(x)) + return isSupported ? url : undefined } export const formatUserColumns = ({ diff --git a/apps/studio/components/interfaces/Home/SecurityStatus.tsx b/apps/studio/components/interfaces/Home/SecurityStatus.tsx index eeba9e89523..be4dc9b4425 100644 --- a/apps/studio/components/interfaces/Home/SecurityStatus.tsx +++ b/apps/studio/components/interfaces/Home/SecurityStatus.tsx @@ -1,6 +1,6 @@ import { CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' import Link from 'next/link' -import { useState } from 'react' +import { Fragment, useState } from 'react' import { useParams } from 'common' import { useProjectLintsQuery } from 'data/lint/lint-query' @@ -81,12 +81,9 @@ export const SecurityStatus = () => { const { label, descriptionShort } = LINT_TABS.find((tab) => tab.id === level) ?? {} return ( lints.length > 0 && ( - <> + -
    +
    @@ -109,7 +106,7 @@ export const SecurityStatus = () => {
    - + ) ) })} diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx index 7c449363541..c682d1e1e53 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -169,7 +169,10 @@ const DiskUsage = ({ const totalDiskUsage = primaryDiskUsage + replicaDiskUsage return ( -
    +
    {project.name} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx index 8b69078bb51..06d0bd6eefe 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx @@ -154,6 +154,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance return ( { if (typeof idx === 'number' && idx >= 0) { setSelectedRow(idx) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx index 8adbf34019c..3cfb338bee8 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -60,15 +60,13 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { return ( - {canUpdateFiles && ( - <> - onSelectCreateFolder(props.index)}> - - New folder - - - - )} + {canUpdateFiles && [ + onSelectCreateFolder(props.index)}> + + New folder + , + , + ]} onSelectAllItemsInColumn(props.index)}> Select all items diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index 479fc46ca2b..5e31b0477ca 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -1,10 +1,10 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { compact, debounce, isEqual, noop } from 'lodash' import { useCallback, useEffect, useRef, useState } from 'react' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import APIDocsButton from 'components/ui/APIDocsButton' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { @@ -426,64 +426,34 @@ const FileExplorerHeader = ({ {/* @ts-ignore */}
    - - - - - {!canUpdateStorage && ( - - - -
    - - You need additional permissions to upload files - -
    -
    -
    - )} -
    - - - - - {!canUpdateStorage && ( - - - -
    - - You need additional permissions to create folders - -
    -
    -
    - )} -
    + } + type="text" + disabled={!canUpdateStorage || breadcrumbs.length === 0} + onClick={onSelectUpload} + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to upload files', + }, + }} + > + Upload files + + } + type="text" + disabled={!canUpdateStorage || breadcrumbs.length === 0} + onClick={() => addNewFolderPlaceholder(-1)} + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to create folders', + }, + }} + > + Create folder +
    diff --git a/apps/studio/data/database/retrieve-index-from-select-query.ts b/apps/studio/data/database/retrieve-index-from-select-query.ts index 06d80dddebc..8ef31cde994 100644 --- a/apps/studio/data/database/retrieve-index-from-select-query.ts +++ b/apps/studio/data/database/retrieve-index-from-select-query.ts @@ -112,6 +112,7 @@ export const useGetIndexesFromSelectQuery = { const { auth_id } = useParams() const [selectedOrgSlug, setSelectedOrgSlug] = useState() - const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + const { + data: organizations, + isSuccess: isSuccessOrganizations, + isLoading: isLoadingOrganizations, + } = useOrganizationsQuery() const { data: requester, isLoading, isError, error } = useApiAuthorizationQuery({ id: auth_id }) const isApproved = (requester?.approved_at ?? null) !== null const isExpired = dayjs().isAfter(dayjs(requester?.expires_at)) @@ -43,10 +47,11 @@ const APIAuthorizationPage: NextPageWithLayout = () => { const isSubmitting = isApproving || isDeclining useEffect(() => { - if (!isLoadingOrganizations) { - setSelectedOrgSlug(organizations?.[0]?.slug ?? undefined) + if (isSuccessOrganizations && organizations.length > 0) { + setSelectedOrgSlug(organizations[0].slug) } - }, [isLoadingOrganizations]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccessOrganizations]) const onApproveRequest = async () => { if (!auth_id) { diff --git a/packages/ui-patterns/info-tooltip.tsx b/packages/ui-patterns/info-tooltip.tsx index ae2f2004fdf..e3de1744883 100644 --- a/packages/ui-patterns/info-tooltip.tsx +++ b/packages/ui-patterns/info-tooltip.tsx @@ -10,9 +10,9 @@ const SVG = forwardRef>((props, ref {...props} > )) From f9016e9128d04ae25cad465db348c63fa6222bba Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 22 Oct 2024 06:12:57 -0230 Subject: [PATCH 18/80] Chore/auth logs timestamps (#30013) * co/auth-logs-timestamps * Opt to use TimestampInfo instead * Revert test data --------- Co-authored-by: Joshen Lim --- apps/studio/components/interfaces/Auth/Users/UserLogs.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx index 6e5636ca8a8..391f65f922d 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx @@ -9,7 +9,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { User } from 'data/auth/users-query' import useLogsPreview from 'hooks/analytics/useLogsPreview' import { Button, cn, CriticalIcon, Separator } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition, TimestampInfo } from 'ui-patterns' import { UserHeader } from './UserHeader' import { PANEL_PADDING } from './UserPanel' @@ -103,7 +103,6 @@ export const UserLogs = ({ user }: UserLogsProps) => {
    {authLogs.map((log) => { const status = ((log.status ?? '-') as any).toString() - const formattedTime = dayjs(log.timestamp).format('DD MMM HH:mm:ss') const is400 = status.startsWith('4') const is500 = status.startsWith('5') @@ -112,8 +111,8 @@ export const UserLogs = ({ user }: UserLogsProps) => { key={log.id} className="flex items-center transition font-mono px-2 py-1.5 bg-surface-100 divide-x" > -

    - {formattedTime} +

    +

    Date: Tue, 22 Oct 2024 09:39:36 -0230 Subject: [PATCH 19/80] new Integrations page (#29671) * Start new integrations page * Move cronjobs into integrations * Rearrange * Remove duplicate * Move around menu items --- .../Wrappers/WrappersDisabledState.tsx | 2 +- .../CronJobs/CreateCronJobSheet.tsx | 3 +- .../CronJobs/CronJobCard.tsx | 0 .../CronJobs/CronJobScheduleSection.tsx | 0 .../CronJobs/CronJobs.constants.tsx | 0 .../CronJobs/CronJobs.utils.test.ts | 0 .../CronJobs/CronJobs.utils.ts | 0 .../CronJobs/CronJobsDisabledState.tsx | 3 +- .../CronJobs/CronJobsListing.tsx | 3 +- .../CronJobs/DeleteCronJob.tsx | 0 .../CronJobs/EdgeFunctionSection.tsx | 0 .../CronJobs/HttpHeaderFieldsSection.tsx | 0 .../CronJobs/HttpParameterFieldsSection.tsx | 0 .../CronJobs/HttpRequestSection.tsx | 0 .../CronJobs/SqlFunctionSection.tsx | 0 .../CronJobs/SqlSnippetSection.tsx | 0 .../Settings/Vault/VaultNavTabs.tsx | 6 +- .../DatabaseLayout/DatabaseMenu.utils.tsx | 16 +-- .../layouts/DocsLayout/DocsLayout.utils.tsx | 5 +- .../ProjectIntegrations.Commands.tsx | 107 ++++++++++++++++++ .../ProjectIntegrationsLayout.tsx | 59 ++++++++++ .../ProjectIntegrationsMenu.utils.tsx | 66 +++++++++++ .../NavigationBar/NavigationBar.utils.tsx | 8 +- ...gsMenu.utils.ts => SettingsMenu.utils.tsx} | 4 +- apps/studio/next.config.js | 30 +++++ .../{database => integrations}/cron-jobs.tsx | 8 +- .../[ref]/{api => integrations}/graphiql.tsx | 6 +- .../{database => integrations}/hooks.tsx | 5 +- .../{settings => integrations}/vault/keys.tsx | 5 +- .../vault/secrets.tsx | 7 +- .../wrappers/[id].tsx | 6 +- .../wrappers/index.tsx | 5 +- .../wrappers/new.tsx | 0 33 files changed, 320 insertions(+), 34 deletions(-) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CreateCronJobSheet.tsx (99%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobCard.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobScheduleSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobs.constants.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobs.utils.test.ts (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobs.utils.ts (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobsDisabledState.tsx (97%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/CronJobsListing.tsx (98%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/DeleteCronJob.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/EdgeFunctionSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/HttpHeaderFieldsSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/HttpParameterFieldsSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/HttpRequestSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/SqlFunctionSection.tsx (100%) rename apps/studio/components/interfaces/{Database => Integrations}/CronJobs/SqlSnippetSection.tsx (100%) create mode 100644 apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx create mode 100644 apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx create mode 100644 apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx rename apps/studio/components/layouts/ProjectSettingsLayout/{SettingsMenu.utils.ts => SettingsMenu.utils.tsx} (96%) rename apps/studio/pages/project/[ref]/{database => integrations}/cron-jobs.tsx (71%) rename apps/studio/pages/project/[ref]/{api => integrations}/graphiql.tsx (94%) rename apps/studio/pages/project/[ref]/{database => integrations}/hooks.tsx (95%) rename apps/studio/pages/project/[ref]/{settings => integrations}/vault/keys.tsx (89%) rename apps/studio/pages/project/[ref]/{settings => integrations}/vault/secrets.tsx (89%) rename apps/studio/pages/project/[ref]/{database => integrations}/wrappers/[id].tsx (75%) rename apps/studio/pages/project/[ref]/{database => integrations}/wrappers/index.tsx (93%) rename apps/studio/pages/project/[ref]/{database => integrations}/wrappers/new.tsx (100%) diff --git a/apps/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx b/apps/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx index 920e6a3577d..61933f017b7 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx @@ -75,7 +75,7 @@ const WrappersDisabledState = () => { return (
    diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobCard.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobScheduleSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobScheduleSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobs.constants.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobs.constants.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobs.utils.test.ts rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobs.utils.ts rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobsDisabledState.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsDisabledState.tsx similarity index 97% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobsDisabledState.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobsDisabledState.tsx index c6003b569d9..4668f703848 100644 --- a/apps/studio/components/interfaces/Database/CronJobs/CronJobsDisabledState.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsDisabledState.tsx @@ -8,8 +8,9 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button } from 'ui' -import EnableExtensionModal from '../Extensions/EnableExtensionModal' + import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' export const CronJobsDisabledState = () => { const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) diff --git a/apps/studio/components/interfaces/Database/CronJobs/CronJobsListing.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx similarity index 98% rename from apps/studio/components/interfaces/Database/CronJobs/CronJobsListing.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx index 916dd0a0612..24a6b8958ba 100644 --- a/apps/studio/components/interfaces/Database/CronJobs/CronJobsListing.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx @@ -10,11 +10,12 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { Button, Sheet, SheetContent } from 'ui' -import EnableExtensionModal from '../Extensions/EnableExtensionModal' + import { CreateCronJobSheet } from './CreateCronJobSheet' import { CronJobCard } from './CronJobCard' import { CronJobsDisabledState } from './CronJobsDisabledState' import DeleteCronJob from './DeleteCronJob' +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' export const CronJobsListing = () => { const { project } = useProjectContext() diff --git a/apps/studio/components/interfaces/Database/CronJobs/DeleteCronJob.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/DeleteCronJob.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/EdgeFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/EdgeFunctionSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/HttpHeaderFieldsSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/HttpParameterFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpParameterFieldsSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/HttpParameterFieldsSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/HttpParameterFieldsSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/HttpRequestSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/HttpRequestSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/SqlFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/SqlFunctionSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx diff --git a/apps/studio/components/interfaces/Database/CronJobs/SqlSnippetSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/CronJobs/SqlSnippetSection.tsx rename to apps/studio/components/interfaces/Integrations/CronJobs/SqlSnippetSection.tsx diff --git a/apps/studio/components/interfaces/Settings/Vault/VaultNavTabs.tsx b/apps/studio/components/interfaces/Settings/Vault/VaultNavTabs.tsx index a79cb9fe45c..27d3f4b16c7 100644 --- a/apps/studio/components/interfaces/Settings/Vault/VaultNavTabs.tsx +++ b/apps/studio/components/interfaces/Settings/Vault/VaultNavTabs.tsx @@ -11,10 +11,12 @@ const VaultNavTabs = (props: PropsWithChildren) => { return ( - Secrets Management + + Secrets Management + - Encryption Keys + Encryption Keys ) diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx index 02f7c059f0f..bf527528b39 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx @@ -101,7 +101,8 @@ export const generateDatabaseMenu = ( { name: 'Wrappers', key: 'wrappers', - url: `/project/${ref}/database/wrappers`, + url: `/project/${ref}/integrations/wrappers`, + rightIcon: , items: [], }, { @@ -115,17 +116,8 @@ export const generateDatabaseMenu = ( { name: 'Webhooks', key: 'hooks', - url: `/project/${ref}/database/hooks`, - items: [], - }, - ] - : []), - ...(!!cronUiEnabled - ? [ - { - name: 'Cron Jobs', - key: 'cron-jobs', - url: `/project/${ref}/database/cron-jobs`, + url: `/project/${ref}/integrations/hooks`, + rightIcon: , items: [], }, ] diff --git a/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx b/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx index f6f69f2c2bf..04aa4edd1e5 100644 --- a/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx +++ b/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx @@ -1,6 +1,6 @@ import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' import { BASE_PATH } from 'lib/constants' -import { Book, BookOpen } from 'lucide-react' +import { ArrowUpRight, Book, BookOpen } from 'lucide-react' import SVG from 'react-inlinesvg' export const generateDocsMenu = ( @@ -71,7 +71,7 @@ export const generateDocsMenu = ( { name: 'GraphiQL', key: 'graphiql', - url: `/project/${ref}/api/graphiql`, + url: `/project/${ref}/integrations/graphiql`, icon: ( ), items: [], + rightIcon: , }, ], }, diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx new file mode 100644 index 00000000000..891f526f234 --- /dev/null +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx @@ -0,0 +1,107 @@ +import { Code } from 'lucide-react' + +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' +import { useParams } from 'common' +import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering' + +export function useDatabaseGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.QUERY, + [ + { + id: 'run-sql', + name: 'Run SQL', + route: `/project/${ref}/sql/new`, + icon: () => , + }, + ], + { + ...options, + deps: [ref], + orderSection: orderCommandSectionsByPriority, + sectionMeta: { priority: 2 }, + } + ) + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-database-tables', + name: 'Tables', + value: 'Database: Tables', + route: `/project/${ref}/database/tables`, + defaultHidden: true, + }, + { + id: 'nav-database-triggers', + name: 'Triggers', + value: 'Database: Triggers', + route: `/project/${ref}/database/triggers`, + defaultHidden: true, + }, + { + id: 'nav-database-functions', + name: 'Functions', + value: 'Database: Functions', + route: `/project/${ref}/database/functions`, + defaultHidden: true, + }, + { + id: 'nav-database-extensions', + name: 'Extensions', + value: 'Database: Extensions', + route: `/project/${ref}/database/extensions`, + defaultHidden: true, + }, + { + id: 'nav-database-roles', + name: 'Roles', + value: 'Database: Roles', + route: `/project/${ref}/database/roles`, + defaultHidden: true, + }, + { + id: 'nav-database-replication', + name: 'Replication', + value: 'Database: Replication', + route: `/project/${ref}/database/replication`, + defaultHidden: true, + }, + { + id: 'nav-database-hooks', + name: 'Webhooks', + value: 'Database: Webhooks', + route: `/project/${ref}/database/hooks`, + defaultHidden: true, + }, + { + id: 'nav-database-backups', + name: 'Backups', + value: 'Database: Backups', + route: `/project/${ref}/database/backups/scheduled`, + defaultHidden: true, + }, + { + id: 'nav-database-wrappers', + name: 'Wrappers', + value: 'Database: Wrappers', + route: `/project/${ref}/database/wrappers`, + defaultHidden: true, + }, + { + id: 'nav-database-migrations', + name: 'Migrations', + value: 'Database: Migrations', + route: `/project/${ref}/database/migrations`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx new file mode 100644 index 00000000000..a7a08d5f617 --- /dev/null +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx @@ -0,0 +1,59 @@ +import { useRouter } from 'next/router' +import { PropsWithChildren } from 'react' + +import { ProductMenu } from 'components/ui/ProductMenu' +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { withAuth } from 'hooks/misc/withAuth' +import ProjectLayout from '../ProjectLayout/ProjectLayout' +import { generateProjectIntegrationsMenu } from './ProjectIntegrationsMenu.utils' +import { useFlag } from 'hooks/ui/useFlag' + +export interface ProjectIntegrationsLayoutProps { + title?: string +} + +const ProjectIntegrationsMenu = () => { + const cronUiEnabled = useFlag('cronUi') + const project = useSelectedProject() + + const router = useRouter() + const page = router.pathname.split('/')[4] + + const { data } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + console.log({ data }) + const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined + const graphqlExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_graphql') !== undefined + + return ( + <> + + + ) +} + +const ProjectIntegrationsLayout = ({ + children, +}: PropsWithChildren) => { + return ( + } + isBlocking={false} + > + {children} + + ) +} + +export default withAuth(ProjectIntegrationsLayout) diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx new file mode 100644 index 00000000000..78f2f91eb6a --- /dev/null +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx @@ -0,0 +1,66 @@ +import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' +import type { Project } from 'data/projects/project-detail-query' + +export const generateProjectIntegrationsMenu = ( + project?: Project, + flags?: { + pgNetExtensionExists: boolean + cronUiEnabled: boolean + graphqlExtensionExists: boolean + } +): ProductMenuGroup[] => { + const ref = project?.ref ?? 'default' + const { pgNetExtensionExists, cronUiEnabled, graphqlExtensionExists } = flags || {} + + return [ + { + title: 'Manage', + items: [ + { + name: 'Wrappers', + key: 'wrappers', + url: `/project/${ref}/integrations/wrappers`, + items: [], + }, + ...(!!pgNetExtensionExists + ? [ + { + name: 'Webhooks', + key: 'hooks', + url: `/project/${ref}/integrations/hooks`, + items: [], + }, + ] + : []), + + ...(!!cronUiEnabled + ? [ + { + name: 'Cron Jobs', + key: 'cron-jobs', + url: `/project/${ref}/integrations/cron-jobs`, + items: [], + }, + ] + : []), + ...(!!graphqlExtensionExists + ? [ + { + name: 'GraphiQL', + key: 'graphiql', + url: `/project/${ref}/integrations/graphiql`, + items: [], + }, + ] + : []), + { + name: 'Vault', + key: 'vault', + url: `/project/${ref}/integrations/vault/secrets`, + items: [], + label: 'BETA', + }, + ], + }, + ] +} diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 3073b168764..159f5bb7dae 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -12,7 +12,7 @@ import { TableEditor, } from 'icons' import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar' -import { Settings, FileText, List, Lightbulb } from 'lucide-react' +import { Settings, FileText, List, Lightbulb, Blocks } from 'lucide-react' export const generateToolRoutes = (ref?: string, project?: Project): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP @@ -138,6 +138,12 @@ export const generateOtherRoutes = (ref?: string, project?: Project): Route[] => icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/api`), }, + { + key: 'integrations', + label: 'Integrations', + icon: , + link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/wrappers`), + }, ] } diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx similarity index 96% rename from apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts rename to apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index 534af51b477..768c38a20c1 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -2,6 +2,7 @@ import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.typ import type { Project } from 'data/projects/project-detail-query' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import type { Organization } from 'types' +import { ArrowUpRight } from 'lucide-react' export const generateSettingsMenu = ( ref?: string, @@ -62,8 +63,9 @@ export const generateSettingsMenu = ( { name: 'Vault', key: 'vault', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/vault/secrets`, + url: isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/vault/secrets`, items: [], + rightIcon: , label: 'BETA', }, ], diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 5b25632526d..6ea759427e0 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -394,6 +394,36 @@ const nextConfig = { source: '/project/:ref/database/performance-advisor', destination: '/project/:ref/advisors/performance', }, + { + permanent: true, + source: '/project/:ref/database/webhooks', + destination: '/project/:ref/integrations/webhooks', + }, + { + permanent: true, + source: '/project/:ref/database/wrappers', + destination: '/project/:ref/integrations/wrappers', + }, + { + permanent: true, + source: '/project/:ref/.....', + destination: '/project/:ref/integrations/cron-jobs', + }, + { + permanent: true, + source: '/project/:ref/api/graphiql', + destination: '/project/:ref/integrations/graphiql', + }, + { + permanent: true, + source: '/project/:ref/settings/vault/secrets', + destination: '/project/:ref/integrations/vault/secrets', + }, + { + permanent: true, + source: '/project/:ref/settings/vault/keys', + destination: '/project/:ref/integrations/vault/keys', + }, ...(process.env.NEXT_PUBLIC_BASE_PATH?.length ? [ { diff --git a/apps/studio/pages/project/[ref]/database/cron-jobs.tsx b/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx similarity index 71% rename from apps/studio/pages/project/[ref]/database/cron-jobs.tsx rename to apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx index f0fdf1c88a9..cb183f11488 100644 --- a/apps/studio/pages/project/[ref]/database/cron-jobs.tsx +++ b/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx @@ -1,7 +1,7 @@ -import { CronJobsListing } from 'components/interfaces/Database/CronJobs/CronJobsListing' -import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import { CronJobsListing } from 'components/interfaces/Integrations/CronJobs/CronJobsListing' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import type { NextPageWithLayout } from 'types' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' const CronJobsPage: NextPageWithLayout = () => { // TODO: Change this to the correct permissions @@ -23,6 +23,8 @@ const CronJobsPage: NextPageWithLayout = () => { ) } -CronJobsPage.getLayout = (page) => {page} +CronJobsPage.getLayout = (page) => ( + {page} +) export default CronJobsPage diff --git a/apps/studio/pages/project/[ref]/api/graphiql.tsx b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx similarity index 94% rename from apps/studio/pages/project/[ref]/api/graphiql.tsx rename to apps/studio/pages/project/[ref]/integrations/graphiql.tsx index 0d7e0bfb9d8..3d34d908c1b 100644 --- a/apps/studio/pages/project/[ref]/api/graphiql.tsx +++ b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx @@ -7,7 +7,6 @@ import { toast } from 'sonner' import { useParams } from 'common' import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import GraphiQL from 'components/interfaces/GraphQL/GraphiQL' -import DocsLayout from 'components/layouts/DocsLayout/DocsLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { Loading } from 'components/ui/Loading' import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' @@ -18,6 +17,7 @@ import { API_URL, IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' import { useGetImpersonatedRole } from 'state/role-impersonation-state' import type { NextPageWithLayout } from 'types' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' const GraphiQLPage: NextPageWithLayout = () => { const { resolvedTheme } = useTheme() @@ -111,6 +111,8 @@ const GraphiQLPage: NextPageWithLayout = () => { return } -GraphiQLPage.getLayout = (page) => {page} +GraphiQLPage.getLayout = (page) => ( + {page} +) export default GraphiQLPage diff --git a/apps/studio/pages/project/[ref]/database/hooks.tsx b/apps/studio/pages/project/[ref]/integrations/hooks.tsx similarity index 95% rename from apps/studio/pages/project/[ref]/database/hooks.tsx rename to apps/studio/pages/project/[ref]/integrations/hooks.tsx index 3bc6f1ad659..31474bccbda 100644 --- a/apps/studio/pages/project/[ref]/database/hooks.tsx +++ b/apps/studio/pages/project/[ref]/integrations/hooks.tsx @@ -16,6 +16,7 @@ import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation' import { useSchemasQuery } from 'data/database/schemas-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' const HooksPage: NextPageWithLayout = () => { const { project } = useProjectContext() @@ -124,6 +125,8 @@ const HooksPage: NextPageWithLayout = () => { ) } -HooksPage.getLayout = (page) => {page} +HooksPage.getLayout = (page) => ( + {page} +) export default HooksPage diff --git a/apps/studio/pages/project/[ref]/settings/vault/keys.tsx b/apps/studio/pages/project/[ref]/integrations/vault/keys.tsx similarity index 89% rename from apps/studio/pages/project/[ref]/settings/vault/keys.tsx rename to apps/studio/pages/project/[ref]/integrations/vault/keys.tsx index 068b46426ed..63e5fd1b4cb 100644 --- a/apps/studio/pages/project/[ref]/settings/vault/keys.tsx +++ b/apps/studio/pages/project/[ref]/integrations/vault/keys.tsx @@ -2,6 +2,7 @@ import { useParams } from 'common' import { EncryptionKeysManagement, VaultToggle } from 'components/interfaces/Settings/Vault' import VaultNavTabs from 'components/interfaces/Settings/Vault/VaultNavTabs' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' import { @@ -54,5 +55,7 @@ const VaultSettingsSecrets: NextPageWithLayout = () => { ) } -VaultSettingsSecrets.getLayout = (page) => {page} +VaultSettingsSecrets.getLayout = (page) => ( + {page} +) export default VaultSettingsSecrets diff --git a/apps/studio/pages/project/[ref]/settings/vault/secrets.tsx b/apps/studio/pages/project/[ref]/integrations/vault/secrets.tsx similarity index 89% rename from apps/studio/pages/project/[ref]/settings/vault/secrets.tsx rename to apps/studio/pages/project/[ref]/integrations/vault/secrets.tsx index 5adf498d416..bd3c1c7eade 100644 --- a/apps/studio/pages/project/[ref]/settings/vault/secrets.tsx +++ b/apps/studio/pages/project/[ref]/integrations/vault/secrets.tsx @@ -1,8 +1,9 @@ import { useParams } from 'common/hooks' import { SecretsManagement, VaultToggle } from 'components/interfaces/Settings/Vault' import VaultNavTabs from 'components/interfaces/Settings/Vault/VaultNavTabs' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' + import { ScaffoldContainer, ScaffoldDescription, @@ -55,5 +56,7 @@ const VaultSettingsSecrets: NextPageWithLayout = () => { ) } -VaultSettingsSecrets.getLayout = (page) => {page} +VaultSettingsSecrets.getLayout = (page) => ( + {page} +) export default VaultSettingsSecrets diff --git a/apps/studio/pages/project/[ref]/database/wrappers/[id].tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/[id].tsx similarity index 75% rename from apps/studio/pages/project/[ref]/database/wrappers/[id].tsx rename to apps/studio/pages/project/[ref]/integrations/wrappers/[id].tsx index d73f76480aa..d9110015d27 100644 --- a/apps/studio/pages/project/[ref]/database/wrappers/[id].tsx +++ b/apps/studio/pages/project/[ref]/integrations/wrappers/[id].tsx @@ -1,7 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { EditWrapper } from 'components/interfaces/Database' -import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' @@ -17,6 +17,8 @@ const DatabaseWrappersNew: NextPageWithLayout = () => { return } -DatabaseWrappersNew.getLayout = (page) => {page} +DatabaseWrappersNew.getLayout = (page) => ( + {page} +) export default DatabaseWrappersNew diff --git a/apps/studio/pages/project/[ref]/database/wrappers/index.tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx similarity index 93% rename from apps/studio/pages/project/[ref]/database/wrappers/index.tsx rename to apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx index 93e3bf73f4a..6c0d2ea7566 100644 --- a/apps/studio/pages/project/[ref]/database/wrappers/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx @@ -19,6 +19,7 @@ import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPe import type { NextPageWithLayout } from 'types' import { Button } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' const DatabaseWrappers: NextPageWithLayout = () => { const { project } = useProjectContext() @@ -80,6 +81,8 @@ const DatabaseWrappers: NextPageWithLayout = () => { ) } -DatabaseWrappers.getLayout = (page) => {page} +DatabaseWrappers.getLayout = (page) => ( + {page} +) export default DatabaseWrappers diff --git a/apps/studio/pages/project/[ref]/database/wrappers/new.tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx similarity index 100% rename from apps/studio/pages/project/[ref]/database/wrappers/new.tsx rename to apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx From d47048dcd6fe81eede090147f01adfaef2664583 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Oct 2024 20:49:20 +0800 Subject: [PATCH 20/80] Update auth hooks to allow selection of fns that return void (#30022) * Update auth hooks to allow selection of fns that return void * Update for only send email hook --- .../interfaces/Auth/Hooks/CreateHookSheet.tsx | 34 ++++++++++++------- .../studio/components/ui/FunctionSelector.tsx | 6 ++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index ca32ae51599..cfc81587e92 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -123,6 +123,11 @@ export const CreateHookSheet = ({ [title] ) + const supportedReturnTypes = + definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED' + ? ['json', 'jsonb', 'void'] + : ['json', 'jsonb'] + const hook: Hook = useMemo(() => { return { ...definition, @@ -288,20 +293,21 @@ export const CreateHookSheet = ({
    - +
    ( ( - + -
    +
    ( ( { - if (func.return_type === 'json' || func.return_type === 'jsonb') { + if (supportedReturnTypes.includes(func.return_type)) { const { value } = convertArgumentTypes(func.argument_types) if (value.length !== 1) return false return value[0].type === 'json' || value[0].type === 'jsonb' @@ -403,7 +409,11 @@ export const CreateHookSheet = ({ No function with a single JSON/B argument
    - and JSON/B return type found in this schema. + and JSON/B + {definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED' + ? ' or void' + : ''}{' '} + return type found in this schema.
    } /> @@ -413,8 +423,8 @@ export const CreateHookSheet = ({ />
    -

    - The following statements will be executed on the function: +

    + The following statements will be executed on the selected function:

    ) : (
    -

    Choose a function...

    +

    Select a function

    )} @@ -124,7 +124,7 @@ const FunctionSelector = ({ No functions found 7 ? 'h-[210px]' : ''}> - {!(functions || []).length && ( + {!functions.length && ( )} - {functions?.map((func) => ( + {functions.map((func) => ( Date: Tue, 22 Oct 2024 11:48:43 -0230 Subject: [PATCH 21/80] Fix wrappers link (#30032) * Fix wrappers link * Update links --- .../interfaces/Database/Wrappers/CreateWrapper.tsx | 4 ++-- .../components/interfaces/Database/Wrappers/EditWrapper.tsx | 6 +++--- .../interfaces/Database/Wrappers/WrappersDropdown.tsx | 2 +- .../interfaces/Database/Wrappers/WrappersEmptyState.tsx | 2 +- .../components/layouts/DatabaseLayout/Database.Commands.tsx | 2 +- .../ProjectIntegrations.Commands.tsx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index 5e7e016d34a..685e05b56a2 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -52,7 +52,7 @@ const CreateWrapper = () => { const hasNewSchema = newTables.some((table) => table.is_new_schema) if (hasNewSchema) invalidateSchemasQuery(queryClient, ref) - router.push(`/project/${ref}/database/wrappers`) + router.push(`/project/${ref}/integrations/wrappers`) }, }) @@ -89,7 +89,7 @@ const CreateWrapper = () => {

    ) diff --git a/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 895d5aa505c..71aed86921c 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -62,7 +62,7 @@ const EditWrapper = () => { const hasNewSchema = wrapperTables.some((table) => table.is_new_schema) if (hasNewSchema) invalidateSchemasQuery(queryClient, ref) - router.push(`/project/${ref}/database/wrappers`) + router.push(`/project/${ref}/integrations/wrappers`) }, }) @@ -117,7 +117,7 @@ const EditWrapper = () => {

    ) @@ -169,7 +169,7 @@ const EditWrapper = () => { 'absolute -left-20 top-1 opacity-75 hover:opacity-100', ].join(' ')} > - +

    Back

    diff --git a/apps/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/apps/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx index 9bb472c3298..8d8a262ae00 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -86,7 +86,7 @@ const WrapperDropdown = ({ buttonText = 'Add wrapper', align = 'end' }: WrapperD onSelect={() => { setOpen(false) router.push( - `/project/${ref}/database/wrappers/new?type=${wrapper.name.toLowerCase()}` + `/project/${ref}/integrations/wrappers/new?type=${wrapper.name.toLowerCase()}` ) }} className="gap-2 cursor-pointer" diff --git a/apps/studio/components/interfaces/Database/Wrappers/WrappersEmptyState.tsx b/apps/studio/components/interfaces/Database/Wrappers/WrappersEmptyState.tsx index 39bc4db5957..3196e7c2cb9 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/WrappersEmptyState.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/WrappersEmptyState.tsx @@ -19,7 +19,7 @@ const WrappersEmptyState = () => {
    {WRAPPERS.map((wrapper) => ( diff --git a/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx index 891f526f234..0fdc8a11075 100644 --- a/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx @@ -91,7 +91,7 @@ export function useDatabaseGotoCommands(options?: CommandOptions) { id: 'nav-database-wrappers', name: 'Wrappers', value: 'Database: Wrappers', - route: `/project/${ref}/database/wrappers`, + route: `/project/${ref}/integrations/wrappers`, defaultHidden: true, }, { diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx index 891f526f234..0fdc8a11075 100644 --- a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrations.Commands.tsx @@ -91,7 +91,7 @@ export function useDatabaseGotoCommands(options?: CommandOptions) { id: 'nav-database-wrappers', name: 'Wrappers', value: 'Database: Wrappers', - route: `/project/${ref}/database/wrappers`, + route: `/project/${ref}/integrations/wrappers`, defaultHidden: true, }, { From 38d994da678976c0dd095c72c86fa863aa079137 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:16:44 +0300 Subject: [PATCH 22/80] Chore: Clarify Disk Terms in the doc (#29955) * Chore: Clarify Disk Metrics in the doc * Add note about Disk size * consolidate terms to disk and database size * fix vale comments * Update database-size.mdx * Update database-size.mdx --------- Co-authored-by: Inian --- .../content/guides/platform/database-size.mdx | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index a455371e9ad..3415a649820 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -1,19 +1,18 @@ --- -id: 'database-size' -title: 'Database size' +id: 'disk-metrics' +title: 'Understanding Database and Disk Size' description: 'Understanding how database size applies to your subscription.' --- -Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your organization's [billing usage](https://supabase.com/dashboard/org/_/usage) and is updated daily. As you read this document we will refer to "database size" and "disk size": +Disk metrics refer to the storage usage reported by Postgres. These metrics are updated daily. As you read through this document, we will refer to "database size" and "disk size": -- "Database size" is the total size of used storage from your database. -- "Disk Storage size" describes the size of the underlying available storage. +- _Database size_: Displays the actual size of the data within your Postgres database. This can be found on the [Database Reports page](https://supabase.com/dashboard/project/_/reports/database). -## Database space management +- _Disk size_: Shows the overall disk space usage, which includes both the database size and additional files required for Postgres to function like the Write Ahead Log (WAL) and other system log files. You can view this on the [Database Settings page](https://supabase.com/dashboard/project/_/settings/database). -### Database size +## Database size -This SQL query will show the current size of your Postgres database: +This SQL query will show the size of all databases in your Postgres cluster: ```sql select @@ -23,7 +22,7 @@ from pg_database; This value is reported in the [database report page](https://supabase.com/dashboard/project/_/reports/database). -Database Space is consumed primarily by your data, indexes, and materialized views. You can reduce your disk size by removing any of these and running a Vacuum operation. +Database size is consumed primarily by your data, indexes, and materialized views. You can reduce your database size by removing any of these and running a Vacuum operation. @@ -69,39 +68,37 @@ Running a manual vacuum after deleting large amounts of data from your DB could New Supabase projects have a database size of ~40-60mb. This space includes pre-installed extensions, schemas, and default Postgres data. Additional database size is used when installing extensions, even if those extensions are inactive. -## Disk management +## Disk size -Supabase uses network-attached storage to balance performance with scalability. The behavior of your disk depends on your billing plan. +Supabase uses network-attached storage to balance performance with scalability. The disk scaling behavior depends on your billing plan. ### Paid plan behavior -Projects on the Pro Plan and above have auto-scaling Disk Storage. +Projects on the Pro Plan and higher have auto-scaling disks. -Disk Storage expands automatically when the database reaches 90% of the disk size. The disk is expanded to be 50% larger (e.g., 8GB -> 12GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode. +Disk size expands automatically when the database reaches 90% of the allocated disk size. The disk is expanded to be 50% larger (for example, 8 GB -> 12 GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode. -The automatic resize operation will add an additional 50% capped to a maximum of 200GB. If 50% of your current usage is more than 200GB then only 200GB will be added to your disk (e.g. a size of 1500GB will resize to 1700GB). +The automatic resize operation will add an additional 50% capped to a maximum of 200 GB. If 50% of your current usage is more than 200 GB then only 200 GB will be added to your disk (for example a size of 1500 GB will resize to 1700 GB). -The Disk Storage Size can also be manually expanded on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). The maximum default you can expand the Disk Storage to is 200GB. If you wish to manually expand it to any more than 200GB, please [contact us](https://supabase.com/dashboard/support/new) to discuss expanding the 200GB limit. +Disk size can also be manually expanded on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). The maximum disk size for the Pro/Team Plan is 60 TB. If you need more than this, [contact us](https://forms.supabase.com/enterprise) to learn more about the Enterprise Plan. -You may want to import a lot of data into your database which requires multiple disk expansions; for example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). If so, it is highly recommended you increase the Disk Storage Size manually on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). +You may want to import a lot of data into your database which requires multiple disk expansions. for example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). If so, it is highly recommended you increase the disk size manually on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). -Due to provider restrictions, disk expansions can occur only once every six hours. During the six-hour window, the disk cannot be resized again. +Due to restrictions on the underlying cloud provider, disk expansions can occur only once every six hours. During the six hour cool down window, the disk cannot be resized again. -The maximum Disk Storage Size for the Pro/Team Plan is 60TB. If you need more than this, [contact us](https://forms.supabase.com/enterprise) to learn more about the Enterprise Plan. - ### Free Plan behavior -Free Plan projects enter [read-only](#read-only-mode) mode when you exceed the 500MB limit. Once in read-only mode, you have several options: +Free Plan projects enter [read-only](#read-only-mode) mode when you exceed the 500 MB limit. Once in read-only mode, you have these options: -- [Upgrade to the Pro Plan](https://supabase.com/dashboard/org/_/billing) to increase the limit to 8GB. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale and expand beyond the 8GB database size limit. +- [Upgrade to the Pro Plan](https://supabase.com/dashboard/org/_/billing) to increase the limit to 8 GB. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale beyond the 8 GB disk size limit. - [Disable read-only mode](#disabling-read-only-mode) and reduce your database size. ### Read-only mode @@ -134,6 +131,4 @@ set default_transaction_read_only = 'off'; ### Reducing disk size -Disks don't automatically downsize during normal operation. Once you have [reduced your database space usage](/docs/guides/platform/database-size#database-space-management), they _will_ automatically "right-size" when a Supabase (or Postgres) update is applied. Their final size after the update is 1.2x the size of the database (min 8 GB). For example, if your database is 100GB in size, and you have a 200GB disk, the size after a Supabase update will be 120GB. - -Learn more about [upgrading your project](/docs/guides/platform/migrating-and-upgrading-projects#upgrade-your-project). +Disks don't automatically downsize during normal operation. Once you have [reduced your database size](/docs/guides/platform/database-size#database-size), they _will_ automatically "right-size" during a [project upgrade](/docs/guides/platform/upgrading). The final disk size after the upgrade is 1.2x the size of the database with a minimum of 8 GB. For example, if your database size is 100GB, and you have a 200GB disk, the size after a project upgrade will be 120 GB. From b2cd52aac858821cec884e7449ac93ba148aec55 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 22 Oct 2024 13:48:55 -0230 Subject: [PATCH 23/80] Chore/disk size helper (#29952) * Add disk size note * Capitalize * Include db size --- .../DiskManagement/DiskSpaceBar.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskSpaceBar.tsx b/apps/studio/components/interfaces/DiskManagement/DiskSpaceBar.tsx index 1f3ff172e64..4c588d05af4 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskSpaceBar.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskSpaceBar.tsx @@ -9,6 +9,10 @@ import { TooltipTrigger_Shadcn_, } from 'ui' import { AUTOSCALING_THRESHOLD } from './DiskManagement.constants' +import { Info } from 'lucide-react' +import { formatBytes } from 'lib/helpers' +import { useDatabaseSizeQuery } from 'data/database/database-size-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' interface DiskSpaceBarProps { showNewBar: boolean @@ -29,6 +33,13 @@ export default function DiskSpaceBar({ const newUsedPercentage = (usedSize / newTotalSize) * 100 const newResizePercentage = AUTOSCALING_THRESHOLD * 100 + const { project } = useProjectContext() + const { data } = useDatabaseSizeQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const databaseSizeBytes = data?.result[0].db_size ?? 0 return (
    @@ -154,8 +165,8 @@ export default function DiskSpaceBar({ > -
    - Autoscaling +
    + Autoscaling
    @@ -186,6 +197,12 @@ export default function DiskSpaceBar({ Available space
    +

    + Note: Disk Size refers to the total space your + project occupies on disk, including the database itself (currently{' '} + {formatBytes(databaseSizeBytes, 2, 'GB')}), additional files like the + write-ahead log (WAL), and other internal resources. +

    ) } From 09757fcac45c477df965b1ffae1411ab9573730c Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 23 Oct 2024 02:33:26 +0800 Subject: [PATCH 24/80] fix: allow sms rate limit update even if autoconfirm is enabled (#30039) --- .../studio/components/interfaces/Auth/RateLimits/RateLimits.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 5c8c22d1a85..bbb3210480f 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -53,7 +53,7 @@ const RateLimits = () => { }) const canUpdateEmailLimit = authConfig?.EXTERNAL_EMAIL_ENABLED && isSmtpEnabled(authConfig) - const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED && !authConfig?.SMS_AUTOCONFIRM + const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED const canUpdateAnonymousUsersRateLimit = authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED const FormSchema = z.object({ From 4d185109ab5f6bce15b80b5e63249eb835896130 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 23 Oct 2024 11:17:18 +0800 Subject: [PATCH 25/80] fix: allow developer role to update buckets (#30024) * fix: allow developer role to update buckets * Fix create bucket perms --------- Co-authored-by: Joshen Lim --- apps/studio/components/layouts/StorageLayout/BucketRow.tsx | 2 +- apps/studio/components/layouts/StorageLayout/StorageMenu.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/layouts/StorageLayout/BucketRow.tsx b/apps/studio/components/layouts/StorageLayout/BucketRow.tsx index b44f233ebb7..1474325de01 100644 --- a/apps/studio/components/layouts/StorageLayout/BucketRow.tsx +++ b/apps/studio/components/layouts/StorageLayout/BucketRow.tsx @@ -34,7 +34,7 @@ const BucketRow = ({ onSelectDeleteBucket = noop, onSelectEditBucket = noop, }: BucketRowProps) => { - const canUpdateBuckets = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const canUpdateBuckets = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') return (
    { const [selectedBucketToEdit, setSelectedBucketToEdit] = useState() const [selectedBucketToEmpty, setSelectedBucketToEmpty] = useState() const [selectedBucketToDelete, setSelectedBucketToDelete] = useState() - const canCreateBuckets = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const canCreateBuckets = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const [sort, setSort] = useLocalStorage<'alphabetical' | 'created-at'>( 'storage-explorer-sort', From c9458d93cf38531e7dcd8baab3800a8111310c77 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 23 Oct 2024 15:42:06 +0800 Subject: [PATCH 26/80] Chore/fix broken links wrappers (#30046) * Fix broken links wrappers * Refactor --- .../Database/Wrappers/CreateWrapper.tsx | 2 +- .../Database/Wrappers/WrapperRow.tsx | 107 +++++++----------- apps/studio/components/ui/ButtonTooltip.tsx | 2 +- .../[ref]/integrations/wrappers/index.tsx | 3 +- 4 files changed, 41 insertions(+), 73 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index 685e05b56a2..98056249503 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -142,7 +142,7 @@ const CreateWrapper = () => { 'absolute -left-20 top-1 opacity-75 hover:opacity-100', ].join(' ')} > - +

    Back

    diff --git a/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index 21c9fce17f9..ffdb8ae54d9 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,14 +1,14 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { partition } from 'lodash' +import { ChevronUp, Edit, ExternalLink, Trash } from 'lucide-react' import Image from 'next/legacy/image' import Link from 'next/link' -import { Button, Collapsible } from 'ui' -import { useParams } from 'common/hooks' +import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { FDW } from 'data/fdw/fdws-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { ChevronUp, Edit, ExternalLink, Trash } from 'lucide-react' +import { Collapsible } from 'ui' import type { WrapperMeta } from './Wrappers.types' interface WrapperRowProps { @@ -131,72 +131,41 @@ const WrapperRow = ({
    -
    - {canManageWrappers ? ( - -
    ) diff --git a/apps/studio/components/ui/ButtonTooltip.tsx b/apps/studio/components/ui/ButtonTooltip.tsx index f1ff45d08e8..23372130b80 100644 --- a/apps/studio/components/ui/ButtonTooltip.tsx +++ b/apps/studio/components/ui/ButtonTooltip.tsx @@ -18,7 +18,7 @@ export const ButtonTooltip = forwardRef< {props.children} - {props.disabled && props.tooltip.content.text !== undefined && ( + {props.tooltip.content.text !== undefined && ( {props.tooltip.content.text} diff --git a/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx index 6c0d2ea7566..6360dd51395 100644 --- a/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx @@ -3,7 +3,7 @@ import { ExternalLink } from 'lucide-react' import { Wrappers } from 'components/interfaces/Database' import WrappersDropdown from 'components/interfaces/Database/Wrappers/WrappersDropdown' -import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, @@ -19,7 +19,6 @@ import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPe import type { NextPageWithLayout } from 'types' import { Button } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' const DatabaseWrappers: NextPageWithLayout = () => { const { project } = useProjectContext() From cb5ae761f343c9b309c25f9b806b39c3cdff3e5e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 23 Oct 2024 16:36:09 +0800 Subject: [PATCH 27/80] Chore/add perms check in projects api query (#30025) * Add perms check in projects API query * Start deprecating use of projects-api-query, move to projects-settings-v2-query * PRETTY * Address feedback * Address feedback --- .../interfaces/App/CommandMenu/ApiKeys.tsx | 15 +++-- .../interfaces/App/CommandMenu/ApiUrl.tsx | 10 +-- .../interfaces/Auth/Users/BanUserModal.tsx | 29 +++++---- .../interfaces/Auth/Users/CreateUserModal.tsx | 21 +++++-- .../interfaces/Docs/Authentication.tsx | 28 +++++---- .../interfaces/Docs/GeneralContent.tsx | 27 +++----- .../interfaces/Docs/Introduction.tsx | 9 ++- .../interfaces/Docs/LangSelector.tsx | 26 ++++---- .../interfaces/Docs/Pages/UserManagement.tsx | 39 ++++++------ .../components/interfaces/Docs/RpcContent.tsx | 39 ++++++++---- .../interfaces/Home/Connect/Connect.tsx | 37 +++-------- .../Home/NewProjectPanel/APIKeys.tsx | 19 +++--- .../ProjectAPIDocs/Content/Introduction.tsx | 29 ++++----- .../layouts/StorageLayout/StorageLayout.tsx | 25 ++++---- .../Storage/StorageSettings/S3Connection.tsx | 7 ++- .../StorageSettings/StorageSettings.utils.ts | 8 +-- apps/studio/data/config/keys.ts | 1 + apps/studio/data/config/project-api-query.ts | 11 ++-- .../data/config/project-settings-query.ts | 1 + .../data/config/project-settings-v2-query.ts | 62 +++++++++++++++++++ apps/studio/pages/project/[ref]/api/index.tsx | 28 +++------ .../project/[ref]/integrations/graphiql.tsx | 11 ++-- 22 files changed, 272 insertions(+), 210 deletions(-) create mode 100644 apps/studio/data/config/project-settings-v2-query.ts diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx index a3c3f2909a8..c86051bba3f 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx @@ -1,7 +1,7 @@ import { Key } from 'lucide-react' import { useMemo } from 'react' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { copyToClipboard } from 'lib/helpers' import { Badge } from 'ui' @@ -23,14 +23,13 @@ export function useApiKeysCommands() { const setPage = useSetPage() const project = useSelectedProject() - const { data: settings } = useProjectApiQuery( + const ref = project?.ref || '_' + + const { data: settings } = useProjectSettingsV2Query( { projectRef: project?.ref }, { enabled: !!project } ) - const ref = project?.ref || '_' - - const anonKey = settings?.autoApiService?.defaultApiKey ?? undefined - const serviceKey = settings?.autoApiService?.serviceApiKey ?? undefined + const { anonKey, serviceKey } = getAPIKeys(settings) const commands = useMemo( () => @@ -40,7 +39,7 @@ export function useApiKeysCommands() { id: 'anon-key', name: `Copy anonymous API key`, action: () => { - copyToClipboard(anonKey ?? '') + copyToClipboard(anonKey.api_key ?? '') setIsOpen(false) }, badge: () => ( @@ -56,7 +55,7 @@ export function useApiKeysCommands() { id: 'service-key', name: `Copy service API key`, action: () => { - copyToClipboard(serviceKey ?? '') + copyToClipboard(serviceKey.api_key ?? '') setIsOpen(false) }, badge: () => ( diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx index 28b36682981..1205a8f3333 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx @@ -1,10 +1,10 @@ import { Link } from 'lucide-react' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { copyToClipboard } from 'lib/helpers' import { Badge } from 'ui' import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' -import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' @@ -12,13 +12,13 @@ export function useApiUrlCommand() { const setIsOpen = useSetCommandMenuOpen() const project = useSelectedProject() - const { data: settings } = useProjectApiQuery( + const { data: settings } = useProjectSettingsV2Query( { projectRef: project?.ref }, { enabled: !!project } ) - const apiUrl = settings?.autoApiService.endpoint - ? `${settings.autoApiService.protocol ?? 'https'}://${settings.autoApiService.endpoint}` + const apiUrl = settings?.app_config?.endpoint + ? `https://${settings.app_config.endpoint}` : undefined useRegisterCommands( diff --git a/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx index 1ae878d8855..d246cd04cd5 100644 --- a/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx @@ -1,10 +1,14 @@ import { zodResolver } from '@hookform/resolvers/zod' import dayjs from 'dayjs' -import { useForm } from 'react-hook-form' -import * as z from 'zod' -import { toast } from 'sonner' import { useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' +import { useParams } from 'common' +import { useUserUpdateMutation } from 'data/auth/user-update-mutation' +import { User } from 'data/auth/users-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { Button, cn, @@ -20,10 +24,6 @@ import { Separator, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { useUserUpdateMutation } from 'data/auth/user-update-mutation' -import { User } from 'data/auth/users-query' -import { useProjectApiQuery } from 'data/config/project-api-query' -import { useParams } from 'common' interface BanUserModalProps { visible: boolean @@ -34,7 +34,7 @@ interface BanUserModalProps { export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => { const { ref: projectRef } = useParams() - const { data: apiData } = useProjectApiQuery({ projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { mutate: updateUser, isLoading: isBanningUser } = useUserUpdateMutation({ onSuccess: (_, vars) => { const bannedUntil = dayjs() @@ -62,17 +62,24 @@ export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => { const bannedUntil = dayjs().add(Number(value), unit).format('DD MMM YYYY HH:mm (ZZ)') const onSubmit = (data: FormType) => { - if (!apiData) { + if (!settings) { return toast.error(`Failed to ban user: Error loading project config`) } else if (user.id === undefined) { return toast.error(`Failed to ban user: User ID not found`) } const durationHours = data.unit === 'hours' ? Number(data.value) : Number(data.value) * 24 - const { protocol, endpoint, serviceApiKey } = apiData.autoApiService + const endpoint = settings.app_config?.endpoint + const serviceApiKey = (settings?.service_api_keys ?? []).find( + (x) => x.tags === 'service_role' + )?.api_key + + if (!endpoint) return toast.error(`Failed to ban user: Unable to retrieve API endpoint`) + if (!serviceApiKey) return toast.error(`Failed to ban user: Unable to retrieve API key`) + updateUser({ projectRef, - protocol, + protocol: 'https', endpoint, serviceApiKey, userId: user.id, diff --git a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx index 90d0e02b97e..3f5cdb41567 100644 --- a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx @@ -1,13 +1,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' import { Lock, Mail } from 'lucide-react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' +import { useParams } from 'common' import { useUserCreateMutation } from 'data/auth/user-create-mutation' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button, @@ -40,7 +40,11 @@ const CreateUserFormSchema = z.object({ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { const { ref: projectRef } = useParams() - const { data, isLoading, isSuccess } = useProjectApiQuery({ projectRef }, { enabled: visible }) + const { + data: settings, + isLoading, + isSuccess, + } = useProjectSettingsV2Query({ projectRef }, { enabled: visible }) const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') @@ -56,8 +60,15 @@ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { if (!isSuccess) { return toast.error(`Failed to create user: Error loading project config`) } - const { protocol, endpoint, serviceApiKey } = data.autoApiService - createUser({ projectRef, endpoint, protocol, serviceApiKey, user: values }) + const endpoint = settings?.app_config?.endpoint + const serviceApiKey = (settings?.service_api_keys ?? []).find( + (key) => key.tags === 'service_role' + )?.api_key + + if (!endpoint) return toast.error(`Failed to create user: Unable to retrieve API endpoint`) + if (!serviceApiKey) return toast.error(`Failed to create user: Unable to retrieve API key`) + + createUser({ projectRef, endpoint, protocol: 'https', serviceApiKey, user: values }) } const form = useForm>({ diff --git a/apps/studio/components/interfaces/Docs/Authentication.tsx b/apps/studio/components/interfaces/Docs/Authentication.tsx index d3a44692039..37d6840a7b0 100644 --- a/apps/studio/components/interfaces/Docs/Authentication.tsx +++ b/apps/studio/components/interfaces/Docs/Authentication.tsx @@ -1,24 +1,30 @@ import Link from 'next/link' -import type { AutoApiService } from 'data/config/project-api-query' +import { useParams } from 'common' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import CodeSnippet from './CodeSnippet' import Snippets from './Snippets' interface AuthenticationProps { - autoApiService: AutoApiService selectedLang: 'bash' | 'js' showApiKey: string } -const Authentication = ({ autoApiService, selectedLang, showApiKey }: AuthenticationProps) => { +const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => { + const { ref: projectRef } = useParams() + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + + const { anonKey, serviceKey } = getAPIKeys(settings) + const endpoint = settings?.app_config?.endpoint ?? '' + // [Joshen] ShowApiKey should really be a boolean, its confusing const defaultApiKey = showApiKey !== 'SUPABASE_KEY' - ? autoApiService?.defaultApiKey ?? 'SUPABASE_CLIENT_API_KEY' + ? anonKey?.api_key ?? 'SUPABASE_CLIENT_API_KEY' : 'SUPABASE_CLIENT_API_KEY' const serviceApiKey = showApiKey !== 'SUPABASE_KEY' - ? autoApiService?.serviceApiKey ?? 'SUPABASE_SERVICE_KEY' + ? serviceKey?.api_key ?? 'SUPABASE_SERVICE_KEY' : 'SUPABASE_SERVICE_KEY' return ( @@ -53,8 +59,7 @@ const Authentication = ({ autoApiService, selectedLang, showApiKey }: Authentica

    We have provided you a Client Key to get started. You will soon be able to add as many keys as you like. You can find the anon key in the{' '} - API Settings{' '} - page. + API Settings page.

    @@ -64,7 +69,7 @@ const Authentication = ({ autoApiService, selectedLang, showApiKey }: Authentica /> @@ -85,8 +90,7 @@ const Authentication = ({ autoApiService, selectedLang, showApiKey }: Authentica

    We have provided you with a Service Key to get started. Soon you will be able to add as many keys as you like. You can find the service_role in the{' '} - API Settings{' '} - page. + API Settings page.

    @@ -96,9 +100,7 @@ const Authentication = ({ autoApiService, selectedLang, showApiKey }: Authentica />
    diff --git a/apps/studio/components/interfaces/Docs/GeneralContent.tsx b/apps/studio/components/interfaces/Docs/GeneralContent.tsx index cb336120919..4be9860fdcb 100644 --- a/apps/studio/components/interfaces/Docs/GeneralContent.tsx +++ b/apps/studio/components/interfaces/Docs/GeneralContent.tsx @@ -4,26 +4,19 @@ import TablesIntroduction from 'components/interfaces/Docs/Pages/Tables/Introduc import UserManagement from 'components/interfaces/Docs/Pages/UserManagement' import RpcIntroduction from 'components/interfaces/Docs/Pages/Rpc/Introduction' -const GeneralContent = ({ autoApiService, selectedLang, page, showApiKey }: any) => { +interface GeneralContentProps { + page?: string + selectedLang: 'bash' | 'js' + showApiKey: string +} + +const GeneralContent = ({ selectedLang, page, showApiKey }: GeneralContentProps) => { let selected = page?.toLowerCase() - if (selected == 'intro' || selected == null) - return + if (selected == 'intro' || selected == null) return if (selected == 'auth') - return ( - - ) + return if (selected == 'users') - return ( - - ) + return if (selected == 'tables-intro') return if (selected == 'rpc-intro') return else diff --git a/apps/studio/components/interfaces/Docs/Introduction.tsx b/apps/studio/components/interfaces/Docs/Introduction.tsx index 6c4c68c167e..b274bc6f388 100644 --- a/apps/studio/components/interfaces/Docs/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Introduction.tsx @@ -5,17 +5,20 @@ import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-co import CodeSnippet from './CodeSnippet' import PublicSchemaNotEnabledAlert from './PublicSchemaNotEnabledAlert' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' interface Props { - autoApiService: AutoApiService selectedLang: 'bash' | 'js' } -export default function Introduction({ autoApiService, selectedLang }: Props) { +export default function Introduction({ selectedLang }: Props) { const { ref: projectRef } = useParams() + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: config } = useProjectPostgrestConfigQuery({ projectRef }) + const endpoint = settings?.app_config?.endpoint ?? '' + const isPublicSchemaEnabled = config?.db_schema .split(',') .map((name) => name.trim()) @@ -24,7 +27,7 @@ export default function Introduction({ autoApiService, selectedLang }: Props) { return (
    - + {!isPublicSchemaEnabled && }
    diff --git a/apps/studio/components/interfaces/Docs/LangSelector.tsx b/apps/studio/components/interfaces/Docs/LangSelector.tsx index 53376c063c9..b523ded9d53 100644 --- a/apps/studio/components/interfaces/Docs/LangSelector.tsx +++ b/apps/studio/components/interfaces/Docs/LangSelector.tsx @@ -1,8 +1,10 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' - -import type { showApiKey } from 'components/interfaces/Docs/Docs.types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Key } from 'lucide-react' + +import { useParams } from 'common' +import type { showApiKey } from 'components/interfaces/Docs/Docs.types' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -15,26 +17,26 @@ const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' } interface LangSelectorProps { selectedLang: string - setSelectedLang: (selectedLang: string) => void showApiKey: showApiKey + setSelectedLang: (selectedLang: string) => void setShowApiKey: (showApiKey: showApiKey) => void - apiKey: string | undefined - autoApiService: any } const LangSelector = ({ selectedLang, - setSelectedLang, showApiKey, + setSelectedLang, setShowApiKey, - apiKey, - autoApiService, }: LangSelectorProps) => { + const { ref: projectRef } = useParams() const canReadServiceKey = useCheckPermissions( PermissionAction.READ, 'service_api_keys.service_role_key' ) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const { anonKey: anonApiKey, serviceKey: serviceApiKey } = getAPIKeys(settings) + return (
    @@ -75,12 +77,12 @@ const LangSelector = ({ setShowApiKey(DEFAULT_KEY)}> hide - {apiKey && ( + {anonApiKey && ( setShowApiKey({ - key: apiKey, + key: anonApiKey.api_key ?? '-', name: 'anon (public)', }) } @@ -93,7 +95,7 @@ const LangSelector = ({ key="service" onClick={() => setShowApiKey({ - key: autoApiService.serviceApiKey, + key: serviceApiKey?.api_key ?? '-', name: 'service_role (secret)', }) } diff --git a/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx b/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx index 9cbf5f4ac2d..c0296107f1b 100644 --- a/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx +++ b/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx @@ -1,7 +1,8 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import type { AutoApiService } from 'data/config/project-api-query' +import { useParams } from 'common' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { makeRandomString } from 'lib/helpers' import CodeSnippet from '../CodeSnippet' import Snippets from '../Snippets' @@ -9,20 +10,18 @@ import Snippets from '../Snippets' const randomPassword = makeRandomString(20) interface UserManagementProps { - autoApiService: AutoApiService selectedLang: 'bash' | 'js' showApiKey: string } -export default function UserManagement({ - autoApiService, - selectedLang, - showApiKey, -}: UserManagementProps) { +export default function UserManagement({ selectedLang, showApiKey }: UserManagementProps) { const router = useRouter() - + const { ref: projectRef } = useParams() const keyToShow = showApiKey ? showApiKey : 'SUPABASE_KEY' + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const endpoint = settings?.app_config?.endpoint ?? '' + return ( <>

    User Management

    @@ -53,7 +52,7 @@ export default function UserManagement({
    @@ -70,7 +69,7 @@ export default function UserManagement({
    @@ -87,7 +86,7 @@ export default function UserManagement({
    @@ -111,7 +110,7 @@ export default function UserManagement({
    @@ -131,7 +130,7 @@ export default function UserManagement({
    @@ -151,7 +150,7 @@ export default function UserManagement({
    @@ -218,7 +217,7 @@ export default function UserManagement({
    @@ -231,7 +230,7 @@ export default function UserManagement({
    @@ -247,7 +246,7 @@ export default function UserManagement({
    @@ -263,7 +262,7 @@ export default function UserManagement({
    @@ -279,7 +278,7 @@ export default function UserManagement({
@@ -300,7 +299,7 @@ export default function UserManagement({
diff --git a/apps/studio/components/interfaces/Docs/RpcContent.tsx b/apps/studio/components/interfaces/Docs/RpcContent.tsx index b7ba1558eb2..c749f6658cd 100644 --- a/apps/studio/components/interfaces/Docs/RpcContent.tsx +++ b/apps/studio/components/interfaces/Docs/RpcContent.tsx @@ -1,38 +1,53 @@ +import { useParams } from 'common' import CodeSnippet from 'components/interfaces/Docs/CodeSnippet' import Description from 'components/interfaces/Docs/Description' import Param from 'components/interfaces/Docs/Param' import Snippets from 'components/interfaces/Docs/Snippets' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { ProjectJsonSchemaPaths } from 'data/docs/project-json-schema-query' /** * TODO: need to support rpc with the same name and different params type */ + +interface RpcContentProps { + rpcId: string + rpcs: { [key: string]: any } + paths?: ProjectJsonSchemaPaths + selectedLang: 'bash' | 'js' + showApiKey: string + refreshDocs: () => void +} + const RpcContent = ({ - autoApiService, rpcId, rpcs, paths, selectedLang, - refreshDocs, showApiKey, -}: any) => { + refreshDocs, +}: RpcContentProps) => { + const { ref: projectRef } = useParams() + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const endpoint = settings?.app_config?.endpoint ?? '' + const meta = rpcs[rpcId] const pathKey = `/rpc/${rpcId}` const path = paths && pathKey in paths ? paths[pathKey] : undefined const keyToShow = !!showApiKey ? showApiKey : 'SUPABASE_KEY' - if (!path) return null - - const { - post: { parameters, summary }, - } = path + const { post } = path! + const { parameters, summary } = post || {} const rpcParamsObject = - parameters && parameters[0] && parameters[0].schema && parameters[0].schema.properties - ? parameters[0].schema.properties + parameters && parameters[0] && parameters[0].schema && (parameters[0].schema as any).properties + ? (parameters[0].schema as any).properties : {} const rpcParams = Object.entries(rpcParamsObject) .map(([k, v]: any) => ({ name: k, ...v })) .filter((x) => !!x.name) + if (!path) return null + return ( <>

@@ -42,7 +57,7 @@ const RpcContent = ({
- +
diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.tsx b/apps/studio/components/interfaces/Home/Connect/Connect.tsx index d00330491b2..2478ade775a 100644 --- a/apps/studio/components/interfaces/Home/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Home/Connect/Connect.tsx @@ -5,10 +5,9 @@ import { useState } from 'react' import { DatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString' import { PoolingModesModal } from 'components/interfaces/Settings/Database/PoolingModesModal' -import { useProjectApiQuery } from 'data/config/project-api-query' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import Panel from 'components/ui/Panel' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' import { Button, DIALOG_PADDING_X, @@ -30,7 +29,6 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C import { getContentFilePath } from './Connect.utils' import ConnectDropdown from './ConnectDropdown' import ConnectTabContent from './ConnectTabContent' -import Panel from 'components/ui/Panel' const Connect = () => { const { ref: projectRef } = useParams() @@ -46,6 +44,9 @@ const Connect = () => { ?.children.find((child) => child.key === selectedChild)?.children[0]?.key || '' ) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') + const handleParentChange = (value: string) => { setSelectedParent(value) @@ -127,32 +128,12 @@ const Connect = () => { return [] } - const { data: projectSettings } = useProjectSettingsQuery({ projectRef }) - - const { data: apiSettings } = useProjectApiQuery({ - projectRef, - }) - - // Get the API service - const apiService = (projectSettings?.services ?? []).find( - (x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID - ) - - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') - - // Get the API service - const apiHost = canReadAPIKeys - ? `${apiSettings?.autoApiService?.protocol ?? 'https'}://${ - apiSettings?.autoApiService?.endpoint ?? '-' - }` - : '' + const endpoint = settings?.app_config?.endpoint + const apiHost = canReadAPIKeys ? `https://${endpoint ?? '-'}` : '' const apiUrl = canReadAPIKeys ? apiHost : null - const anonKey = canReadAPIKeys - ? apiService?.service_api_keys.find((key) => key.tags === 'anon')?.api_key ?? null - : null - - const projectKeys = { apiUrl, anonKey } + const { anonKey } = canReadAPIKeys ? getAPIKeys(settings) : { anonKey: null } + const projectKeys = { apiUrl, anonKey: anonKey?.api_key ?? null } const filePath = getContentFilePath({ connectionObject, diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index 93af608f4e7..afccbce6923 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -3,14 +3,14 @@ import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' import Link from 'next/link' import { useState } from 'react' -import { useParams } from 'common/hooks' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' +import { useParams } from 'common/hooks' import Panel from 'components/ui/Panel' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Input } from 'ui' import { AlertCircle, Loader } from 'lucide-react' +import { Input } from 'ui' const generateInitSnippet = (endpoint: string) => ({ js: ` @@ -42,7 +42,7 @@ const APIKeys = () => { data: settings, isError: isProjectSettingsError, isLoading: isProjectSettingsLoading, - } = useProjectApiQuery({ + } = useProjectSettingsV2Query({ projectRef, }) @@ -55,17 +55,16 @@ const APIKeys = () => { const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') - // Get the API service - const apiService = settings?.autoApiService - const apiKeys = apiService?.service_api_keys ?? [] + const apiKeys = settings?.service_api_keys ?? [] // API keys should not be empty. However it can be populated with a delay on project creation const isApiKeysEmpty = apiKeys.length === 0 const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated - const apiUrl = `${apiService?.protocol ?? 'https'}://${apiService?.endpoint ?? '-'}` - const anonKey = apiKeys.find((key) => key.tags === 'anon') + const endpoint = settings?.app_config?.endpoint + const apiUrl = `https://${endpoint ?? '-'}` + const { anonKey } = getAPIKeys(settings) const clientInitSnippet: any = generateInitSnippet(apiUrl) const selectedLanguageSnippet = clientInitSnippet[selectedLanguage.key] ?? 'No snippet available' @@ -146,7 +145,7 @@ const APIKeys = () => { ? 'JWT secret update failed, new API key may have issues' : jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating ? 'Updating JWT secret...' - : apiService?.defaultApiKey + : anonKey?.api_key } onChange={() => {}} descriptionText={ diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx index 4c50ce99911..10edf7179ef 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/Content/Introduction.tsx @@ -1,17 +1,17 @@ import { useParams } from 'common' import { Button, Input } from 'ui' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { copyToClipboard } from 'lib/helpers' +import { Copy } from 'lucide-react' import { useEffect, useState } from 'react' import ContentSnippet from '../ContentSnippet' import { DOCS_CONTENT } from '../ProjectAPIDocs.constants' import type { ContentProps } from './Content.types' -import { Copy } from 'lucide-react' const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => { const { ref } = useParams() - const { data } = useProjectApiQuery({ projectRef: ref }) + const { data } = useProjectSettingsV2Query({ projectRef: ref }) const [copied, setCopied] = useState<'anon' | 'service'>() @@ -19,9 +19,10 @@ const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => if (copied !== undefined) setTimeout(() => setCopied(undefined), 2000) }, [copied]) - const serviceKey = showKeys - ? data?.autoApiService.serviceApiKey ?? 'SUPABASE_CLIENT_SERVICE_KEY' - : 'SUPABASE_CLIENT_SERVICE_KEY' + const anonApiKey = (data?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key + const serviceApiKey = + (data?.service_api_keys ?? []).find((key) => key.tags === 'service_role')?.api_key ?? + 'SUPABASE_CLIENT_SERVICE_KEY' return ( <> @@ -52,9 +53,7 @@ const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => icon={} onClick={() => { setCopied('anon') - copyToClipboard( - data?.autoApiService.defaultApiKey ?? 'SUPABASE_CLIENT_ANON_KEY' - ) + copyToClipboard(anonApiKey ?? 'SUPABASE_CLIENT_ANON_KEY') }} > {copied === 'anon' ? 'Copied' : 'Copy'} @@ -68,7 +67,11 @@ const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => disabled readOnly size="small" - value={showKeys ? serviceKey : 'Reveal API keys via dropdown in the header'} + value={ + showKeys + ? serviceApiKey ?? 'SUPABASE_CLIENT_SERVICE_KEY' + : 'Reveal API keys via dropdown in the header' + } className="w-full" descriptionText={

@@ -83,9 +86,7 @@ const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => icon={} onClick={() => { setCopied('service') - copyToClipboard( - data?.autoApiService.serviceApiKey ?? 'SUPABASE_CLIENT_SERVICE_KEY' - ) + copyToClipboard(serviceApiKey) }} > {copied === 'service' ? 'Copied' : 'Copy'} @@ -105,7 +106,7 @@ const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index aa97b8fa2b8..0df1670f64f 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -2,7 +2,7 @@ import { useParams } from 'common' import { ReactNode, useEffect } from 'react' import { toast } from 'sonner' -import { AutoApiService, useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { PROJECT_STATUS } from 'lib/constants' @@ -20,25 +20,24 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { const project = useSelectedProject() const storageExplorerStore = useStorageStore() - const { data: settings, isLoading } = useProjectApiQuery({ projectRef }) - const apiService = settings?.autoApiService + const { data: settings, isLoading } = useProjectSettingsV2Query({ projectRef }) + const endpoint = settings?.app_config?.endpoint + const serviceApiKey = (settings?.service_api_keys ?? []).find( + (key) => key.tags === 'service_role' + )?.api_key const isPaused = project?.status === PROJECT_STATUS.INACTIVE useEffect(() => { - if (!isLoading && apiService) initializeStorageStore(apiService) - }, [isLoading, projectRef]) + if (!isLoading && endpoint && serviceApiKey) initializeStorageStore(endpoint, serviceApiKey) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading, projectRef, endpoint, serviceApiKey]) - const initializeStorageStore = async (apiService: AutoApiService) => { + const initializeStorageStore = async (endpoint: string, serviceApiKey: string) => { if (isPaused) return - if (apiService.endpoint) { - storageExplorerStore.initStore( - projectRef!, - apiService.endpoint, - apiService.serviceApiKey, - apiService.protocol - ) + if (endpoint) { + storageExplorerStore.initStore(projectRef!, endpoint, serviceApiKey, 'https') } else { toast.error( 'Failed to fetch project configuration. Try refreshing your browser, or reach out to us at support@supabase.io' diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx index 25a5c517e49..1e58b14c992 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx @@ -12,7 +12,7 @@ import Table from 'components/to-be-cleaned/Table' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, WarningIcon } from 'ui' @@ -35,14 +35,15 @@ export const S3Connection = () => { const canReadS3Credentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') - const { data: projectAPI } = useProjectApiQuery({ projectRef: projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: storageCreds, ...storageCredsQuery } = useStorageCredentialsQuery( { projectRef }, { enabled: canReadS3Credentials } ) + const endpoint = settings?.app_config?.endpoint const hasStorageCreds = storageCreds?.data && storageCreds.data.length > 0 - const s3connectionUrl = getConnectionURL(projectRef ?? '', projectAPI) + const s3connectionUrl = getConnectionURL(projectRef ?? '', endpoint) return ( <> diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts index 44cf98726af..3dab5cc0cd4 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts @@ -26,12 +26,8 @@ export const convertToBytes = (size: number, unit: StorageSizeUnits = StorageSiz return size * Math.pow(k, i) } -export function getConnectionURL(projectRef: string, projectAPI?: ProjectApiResponse) { - if (projectAPI === undefined) return '' - - const projUrl = projectAPI - ? `${projectAPI.autoApiService.protocol}://${projectAPI.autoApiService.endpoint}` - : `https://${projectRef}.supabase.co` +export function getConnectionURL(projectRef: string, endpoint?: string) { + const projUrl = endpoint ? `https://${endpoint}` : `https://${projectRef}.supabase.co` const url = new URL(projUrl) url.pathname = '/storage/v1/s3' diff --git a/apps/studio/data/config/keys.ts b/apps/studio/data/config/keys.ts index 6f4b61da217..a2aab7748f1 100644 --- a/apps/studio/data/config/keys.ts +++ b/apps/studio/data/config/keys.ts @@ -2,6 +2,7 @@ export const configKeys = { pgBouncerStatus: (projectRef: string | undefined) => ['projects', projectRef, 'pgbouncer'] as const, settings: (projectRef: string | undefined) => ['projects', projectRef, 'settings'] as const, + settingsV2: (projectRef: string | undefined) => ['projects', projectRef, 'settings-v2'] as const, api: (projectRef: string | undefined) => ['projects', projectRef, 'settings', 'api'] as const, postgrest: (projectRef: string | undefined) => ['projects', projectRef, 'postgrest'] as const, jwtSecretUpdatingStatus: (projectRef: string | undefined) => diff --git a/apps/studio/data/config/project-api-query.ts b/apps/studio/data/config/project-api-query.ts index 8baaee61d0d..3826300f630 100644 --- a/apps/studio/data/config/project-api-query.ts +++ b/apps/studio/data/config/project-api-query.ts @@ -6,6 +6,8 @@ import { useProjectByRef } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import type { ResponseError } from 'types' import { configKeys } from './keys' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { PermissionAction } from '@supabase/shared-types/out/constants' export type ProjectApiVariables = { projectRef?: string @@ -32,24 +34,25 @@ export async function getProjectApi({ projectRef }: ProjectApiVariables, signal? export type ProjectApiData = Awaited> export type ProjectApiError = ResponseError +/** @deprecated */ export const useProjectApiQuery = ( { projectRef }: ProjectApiVariables, { enabled = true, ...options }: UseQueryOptions = {} ) => { + const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') + return useQuery( configKeys.api(projectRef), ({ signal }) => getProjectApi({ projectRef }, signal), { enabled: enabled && typeof projectRef !== 'undefined', refetchInterval(data) { - if (!data) { - return false - } + if (!data) return false const { autoApiService } = data as unknown as ProjectApiData const apiKeys = autoApiService?.service_api_keys ?? [] - const interval = apiKeys.length === 0 ? 2000 : 0 + const interval = canReadAPIKeys && apiKeys.length === 0 ? 2000 : 0 return interval }, diff --git a/apps/studio/data/config/project-settings-query.ts b/apps/studio/data/config/project-settings-query.ts index 96a3a82385d..7d9d642c132 100644 --- a/apps/studio/data/config/project-settings-query.ts +++ b/apps/studio/data/config/project-settings-query.ts @@ -42,6 +42,7 @@ export async function getProjectSettings( export type ProjectSettingsData = Awaited> export type ProjectSettingsError = ResponseError +/** @deprecated */ export const useProjectSettingsQuery = ( { projectRef }: ProjectSettingsVariables, { diff --git a/apps/studio/data/config/project-settings-v2-query.ts b/apps/studio/data/config/project-settings-v2-query.ts new file mode 100644 index 00000000000..129d756c0bd --- /dev/null +++ b/apps/studio/data/config/project-settings-v2-query.ts @@ -0,0 +1,62 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import type { components } from 'data/api' +import { get, handleError } from 'data/fetchers' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import type { ResponseError } from 'types' +import { configKeys } from './keys' + +export type ProjectSettingsVariables = { projectRef?: string } +export type ProjectSettings = components['schemas']['ProjectSettingsResponse'] + +export async function getProjectSettings( + { projectRef }: ProjectSettingsVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await get('/platform/projects/{ref}/settings', { + params: { path: { ref: projectRef } }, + signal, + }) + + if (error) handleError(error) + return data as unknown as ProjectSettings +} + +type ProjectSettingsData = Awaited> +type ProjectSettingsError = ResponseError + +export const useProjectSettingsV2Query = ( + { projectRef }: ProjectSettingsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => { + // [Joshen] Sync with API perms checking here - shouldReturnApiKeys + // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/projects/ref/settings.controller.ts#L92 + const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*') + + return useQuery( + configKeys.settingsV2(projectRef), + ({ signal }) => getProjectSettings({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + refetchInterval(data) { + const apiKeys = (data as ProjectSettings)?.service_api_keys ?? [] + const interval = canReadAPIKeys && apiKeys.length === 0 ? 2000 : 0 + return interval + }, + ...options, + } + ) +} + +export const getAPIKeys = (settings?: ProjectSettings) => { + const anonKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'anon') + const serviceKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'service_role') + + return { anonKey, serviceKey } +} diff --git a/apps/studio/pages/project/[ref]/api/index.tsx b/apps/studio/pages/project/[ref]/api/index.tsx index d195fd6da37..a693135da06 100644 --- a/apps/studio/pages/project/[ref]/api/index.tsx +++ b/apps/studio/pages/project/[ref]/api/index.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { GeneralContent, ResourceContent, RpcContent } from 'components/interfaces/Docs' import LangSelector from 'components/interfaces/Docs/LangSelector' import DocsLayout from 'components/layouts/DocsLayout/DocsLayout' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query' import { snakeToCamel } from 'lib/helpers' @@ -26,10 +26,7 @@ const DocView = () => { const [selectedLang, setSelectedLang] = useState('js') const [showApiKey, setShowApiKey] = useState(DEFAULT_KEY) - const { data, error } = useProjectApiQuery({ projectRef }) - const apiService = data?.autoApiService - const anonKey = apiService?.defaultApiKey ?? undefined - + const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef }) const { data: jsonSchema, error: jsonSchemaError, @@ -43,11 +40,10 @@ const DocView = () => { const endpoint = customDomainData?.customDomain?.status === 'active' ? `https://${customDomainData.customDomain?.hostname}` - : `${data?.autoApiService.protocol ?? 'https'}://${data?.autoApiService.endpoint ?? '-'}` + : `https://${settings?.app_config?.endpoint ?? '-'}` const { paths } = jsonSchema || {} const PAGE_KEY: any = resource || rpc || page || 'index' - const autoApiService = { ...(data?.autoApiService ?? {}), endpoint } const { resources, rpcs } = Object.entries(paths || {}).reduce( (a, [name]) => { @@ -84,18 +80,18 @@ const DocView = () => { { resources: {}, rpcs: {} } ) - if (error || jsonSchemaError) { + if (settingsError || jsonSchemaError) { return (

Error connecting to API

-

{`${error || jsonSchemaError}`}

+

{`${settingsError || jsonSchemaError}`}

) } - if (isLoading || !data || !jsonSchema) { + if (isLoading || !settings || !jsonSchema) { return (

Building docs ...

@@ -109,11 +105,9 @@ const DocView = () => {
@@ -128,7 +122,6 @@ const DocView = () => { /> ) : rpc ? ( { refreshDocs={refreshDocs} /> ) : ( - + )}
diff --git a/apps/studio/pages/project/[ref]/integrations/graphiql.tsx b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx index 3d34d908c1b..496ba9cb95c 100644 --- a/apps/studio/pages/project/[ref]/integrations/graphiql.tsx +++ b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx @@ -10,8 +10,8 @@ import GraphiQL from 'components/interfaces/GraphQL/GraphiQL' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { Loading } from 'components/ui/Loading' import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' -import { useProjectApiQuery } from 'data/config/project-api-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { API_URL, IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' @@ -32,12 +32,11 @@ const GraphiQLPage: NextPageWithLayout = () => { const pgGraphqlExtension = (data ?? []).find((ext) => ext.name === 'pg_graphql') const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM }) - const { data: settings, isFetched } = useProjectApiQuery({ projectRef }) + const { data: settings, isFetched } = useProjectSettingsV2Query({ projectRef }) - const apiService = settings?.autoApiService - const serviceRoleKey = apiService?.service_api_keys.find((x) => x.name === 'service_role key') - ? apiService.serviceApiKey - : undefined + const serviceRoleKey = (settings?.service_api_keys ?? []).find( + (x) => x.name === 'service_role key' + )?.api_key const { data: config } = useProjectPostgrestConfigQuery({ projectRef }) const jwtSecret = config?.jwt_secret From f521109ce276901d14322a027b6a6b3c8069d943 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 23 Oct 2024 16:43:38 +0800 Subject: [PATCH 28/80] Chore/database functions v2 (#29969) * Init AiAssistantPanel * Temp * Temp * iterating on assistant ux * More spiking * Tweaking... * Moar tweaking.. * Even more tweaking * Tweek * More tweaks * Final tweaking... * TWEAK * Spelling * Fix * Assistant error handling and small bug fix in context for message * Add feature flag * Fix * Address all feedback * Minor --- .../FeaturePreview/APISidePanelPreview.tsx | 4 +- .../App/FeaturePreview/CLSPreview.tsx | 4 +- .../FeaturePreview/FeaturePreviewContext.tsx | 55 +- .../FeaturePreview/FeaturePreviewModal.tsx | 46 +- .../FunctionsAssistantPreview.tsx | 54 ++ .../AIPolicyEditorPanel/RLSCodeEditor.tsx | 3 - .../PolicyTableRow/PolicyTableRowHeader.tsx | 2 +- .../Functions/FunctionsList/FunctionsList.tsx | 65 +- .../Database/Indexes/CreateIndexSidePanel.tsx | 2 +- .../RealtimeFilterPopover/TableSelector.tsx | 2 +- .../AiAssistantPanel/AiMessagePre.tsx | 2 +- .../SQLEditor/AiAssistantPanel/Message.tsx | 2 +- .../interfaces/TableGridEditor/EmptyState.tsx | 2 +- .../TableEditorLayout/TableEditorMenu.tsx | 2 +- .../ui/AIAssistantPanel/AIAssistant.tsx | 696 ++++++++++++++++++ .../ui/AIAssistantPanel/AIAssistant.types.ts | 2 + .../ui/AIAssistantPanel/AIAssistant.utils.ts | 125 ++++ .../ui/AIAssistantPanel/AIAssistantPanel.tsx | 439 +++++++++++ .../AIAssistantPanel/AiAssistant.constants.ts | 10 + .../ui/AIAssistantPanel/ContextBadge.tsx | 33 + .../AIAssistantPanel/DropdownScrollArea.tsx | 36 + .../AIAssistantPanel/EntitiesDropdownMenu.tsx | 110 +++ .../ui/AIAssistantPanel/Message.tsx | 146 ++++ .../ui/AIAssistantPanel/MessagePre.tsx | 113 +++ .../AIAssistantPanel/SchemasDropdownMenu.tsx | 61 ++ .../components/ui/CodeEditor/CodeEditor.tsx | 215 ++++-- apps/studio/components/ui/SchemaComboBox.tsx | 3 +- .../data/database/entity-definitions-query.ts | 10 +- .../entity-types-infinite-query.ts | 21 +- apps/studio/data/entity-types/keys.ts | 2 +- apps/studio/lib/constants/index.ts | 3 + apps/studio/pages/_app.tsx | 2 + apps/studio/pages/api/ai/sql/generate-v2.ts | 6 +- .../project/[ref]/logs/explorer/index.tsx | 4 +- .../img/previews/functions-assistant.png | Bin 0 -> 275225 bytes apps/studio/state/app-state.ts | 12 + packages/ai-commands/src/sql/chat.ts | 44 +- .../AssistantChat/AssistantChatForm.tsx | 7 +- packages/ui-patterns/admonition.tsx | 4 +- 39 files changed, 2182 insertions(+), 167 deletions(-) create mode 100644 apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts create mode 100644 apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts create mode 100644 apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts create mode 100644 apps/studio/components/ui/AIAssistantPanel/ContextBadge.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/DropdownScrollArea.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/EntitiesDropdownMenu.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/Message.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/MessagePre.tsx create mode 100644 apps/studio/components/ui/AIAssistantPanel/SchemasDropdownMenu.tsx create mode 100644 apps/studio/public/img/previews/functions-assistant.png diff --git a/apps/studio/components/interfaces/App/FeaturePreview/APISidePanelPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/APISidePanelPreview.tsx index 5e96abfb824..9a4bde83334 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/APISidePanelPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/APISidePanelPreview.tsx @@ -3,7 +3,7 @@ import { Markdown } from 'components/interfaces/Markdown' import { BASE_PATH } from 'lib/constants' import Image from 'next/image' -const APISidePanelPreview = () => { +export const APISidePanelPreview = () => { const { ref } = useParams() return ( @@ -38,5 +38,3 @@ const APISidePanelPreview = () => {
) } - -export default APISidePanelPreview diff --git a/apps/studio/components/interfaces/App/FeaturePreview/CLSPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/CLSPreview.tsx index 56293f7b9b0..a88c1eddc96 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/CLSPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/CLSPreview.tsx @@ -5,7 +5,7 @@ import { Markdown } from 'components/interfaces/Markdown' import { BASE_PATH } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, WarningIcon } from 'ui' -const CLSPreview = () => { +export const CLSPreview = () => { const { ref } = useParams() return ( @@ -53,5 +53,3 @@ const CLSPreview = () => {

) } - -export default CLSPreview diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index a77d22b887d..84aa5f6c7fe 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -3,6 +3,33 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { EMPTY_OBJ } from 'lib/void' +import { APISidePanelPreview } from './APISidePanelPreview' +import { CLSPreview } from './CLSPreview' +import { FunctionsAssistantPreview } from './FunctionsAssistantPreview' + +export const FEATURE_PREVIEWS = [ + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT, + name: 'Database Functions Assistant', + content: , + discussionsUrl: undefined, + isNew: true, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL, + name: 'Project API documentation', + content: , + discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038', + isNew: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS, + name: 'Column-level privileges', + content: , + discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295', + isNew: false, + }, +] type FeaturePreviewContextType = { flags: { [key: string]: boolean } @@ -17,22 +44,19 @@ const FeaturePreviewContext = createContext({ export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext) export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => { - const [flags, setFlags] = useState({ - [LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]: false, - [LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: false, - [LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: false, - }) + const [flags, setFlags] = useState(() => + FEATURE_PREVIEWS.reduce((a, b) => { + return { ...a, [b.key]: false } + }, {}) + ) useEffect(() => { if (typeof window !== 'undefined') { - setFlags({ - [LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]: - localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT) === 'true', - [LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: - localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL) === 'true', - [LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: - localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS) === 'true', - }) + setFlags( + FEATURE_PREVIEWS.reduce((a, b) => { + return { ...a, [b.key]: localStorage.getItem(b.key) === 'true' } + }, {}) + ) } }, []) @@ -61,3 +85,8 @@ export const useIsColumnLevelPrivilegesEnabled = () => { const { flags } = useFeaturePreviewContext() return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS] } + +export const useIsDatabaseFunctionsAssistantEnabled = () => { + const { flags } = useFeaturePreviewContext() + return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT] +} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index de6979679e8..01cbee09063 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -3,38 +3,23 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' -import { Button, Modal, ScrollArea, cn } from 'ui' -import APISidePanelPreview from './APISidePanelPreview' -import CLSPreview from './CLSPreview' -import { useFeaturePreviewContext } from './FeaturePreviewContext' +import { Badge, Button, Modal, ScrollArea, cn } from 'ui' +import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext' +import { useFlag } from 'hooks/ui/useFlag' const FeaturePreviewModal = () => { - // [Ivan] We should probably move this to a separate file, together with LOCAL_STORAGE_KEYS. We should make adding new feature previews as simple as possible. - const FEATURE_PREVIEWS: { key: string; name: string; content: any; discussionsUrl?: string }[] = [ - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL, - name: 'Project API documentation', - content: , - discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038', - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS, - name: 'Column-level privileges', - content: , - discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295', - }, - ] - const snap = useAppStateSnapshot() const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() + const enableFunctionsAssistant = useFlag('functionsAssistantV2') const selectedFeaturePreview = snap.selectedFeaturePreview === '' ? FEATURE_PREVIEWS[0].key : snap.selectedFeaturePreview const [selectedFeatureKey, setSelectedFeatureKey] = useState(selectedFeaturePreview) + const isNotReleased = + selectedFeatureKey === 'supabase-ui-functions-assistant' && !enableFunctionsAssistant // this modal can be triggered on other pages // Update local state when valtio state changes @@ -103,8 +88,11 @@ const FeaturePreviewModal = () => {
-

{selectedFeature?.name}

-
+
+

{selectedFeature?.name}

+ {selectedFeature?.isNew && New} +
+
{selectedFeature?.discussionsUrl !== undefined && ( )} - + {isNotReleased ? ( + + ) : ( + + )}
{selectedFeature?.content} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx new file mode 100644 index 00000000000..3b48059d6bd --- /dev/null +++ b/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx @@ -0,0 +1,54 @@ +import { useParams } from 'common' +import { Markdown } from 'components/interfaces/Markdown' +import { BASE_PATH } from 'lib/constants' +import { detectOS } from 'lib/helpers' +import Image from 'next/image' + +export const FunctionsAssistantPreview = () => { + const os = detectOS() + const { ref } = useParams() + + return ( +
+

+ We're providing an additional alternative UX to creating database functions through the + dashboard with the integration of our AI Assistant that you might have seen in the Auth + Policies section. +

+ api-docs-side-panel-preview +

+ This preview also shares an improved Assistant interface where you may provide the Assistant + with contexts in hopes to generate more relevant and higher quality outputs. Contexts that + you may provide include specific schemas, and / or specific tables from your database. +

+

+ We'd hope to use this as a consistent pattern throughout the dashboard eventually if this + feature preview proves itself to benefit most of our users, so as usual please do feel free + to let us know what you think if the attached GitHub discussion above! +

+
+

Enabling this preview will:

+
    +
  • + +
  • +
  • + +
  • +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx index 1323ec5c305..e27f8699a4d 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx @@ -68,9 +68,6 @@ const RLSCodeEditor = ({ lineNumbersMinChars: 4, folding: undefined, scrollBeyondLastLine: false, - padding: { - top: editView === 'conversation' ? 20 : 0, - }, } const onMount: OnMount = async (editor, monaco) => { diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index 87b385513f5..fafc704b6ea 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -111,7 +111,7 @@ const PolicyTableRowHeader = ({ - + {!canToggleRLS ? 'You need additional permissions to create RLS policies' : 'Create with Supabase Assistant'} diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 759a4dac823..65f4bf35f2c 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -17,9 +17,18 @@ import { useSchemasQuery } from 'data/database/schemas-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' -import { Input } from 'ui' +import { + AiIconAnimation, + Button, + Input, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' import ProtectedSchemaWarning from '../../ProtectedSchemaWarning' import FunctionList from './FunctionList' +import { useAppStateSnapshot } from 'state/app-state' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' interface FunctionsListProps { createFunction: () => void @@ -32,10 +41,13 @@ const FunctionsList = ({ editFunction = noop, deleteFunction = noop, }: FunctionsListProps) => { - const { project } = useProjectContext() const router = useRouter() const { search } = useParams() + const { project } = useProjectContext() + const { setAiAssistantPanel } = useAppStateSnapshot() + const enableFunctionsAssistant = useIsDatabaseFunctionsAssistantEnabled() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() + const filterString = search ?? '' const setFilterString = (str: string) => { @@ -122,20 +134,41 @@ const FunctionsList = ({ />
- {!isLocked && ( - createFunction()} - tooltip={{ - content: { - side: 'bottom', - text: 'You need additional permissions to create functions', - }, - }} - > - Create a new function - - )} +
+ {!isLocked && ( + createFunction()} + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to create functions', + }, + }} + > + Create a new function + + )} + {enableFunctionsAssistant && ( + + +
{isLocked && } diff --git a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 9faecac311e..983bb4f4283 100644 --- a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -61,7 +61,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = connectionString: project?.connectionString, }) const { data: entities, isLoading: isLoadingEntities } = useEntityTypesQuery({ - schema: selectedSchema, + schemas: [selectedSchema], sort: 'alphabetical', search: searchTerm, projectRef: project?.ref, diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx index 921598b6d12..4161433b7b8 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx @@ -47,7 +47,7 @@ const TableSelector = ({ projectRef: project?.ref, search: searchInput, connectionString: project?.connectionString, - schema: selectedSchemaName, + schemas: [selectedSchemaName], }) useEffect(() => { if (!initiallyLoaded && isSuccess) { diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx index fa82c7a654f..8b2003a699a 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx @@ -54,7 +54,7 @@ export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) '!bg-transparent !py-3 !px-3.5 prose dark:prose-dark', // change the look of the code block. The flex hack is so that the code is wrapping since // every word is a separate span - '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap' + '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block' )} hideCopy hideLineNumbers diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx index edd29cd1987..e7bbd9dd31c 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx @@ -30,7 +30,7 @@ const Message = memo(function Message({ isSelected = false, onDiff = noop, children, - action = <>, + action = null, }: PropsWithChildren) { const { profile } = useProfile() diff --git a/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx b/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx index bd8f0a90362..07a77e633d8 100644 --- a/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx @@ -27,7 +27,7 @@ const EmptyState = ({}: EmptyStateProps) => { const { data } = useEntityTypesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schemas: [selectedSchema], sort, }) diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 1e985ecf34d..c859e571e41 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -67,7 +67,7 @@ const TableEditorMenu = () => { { projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schemas: [selectedSchema], search: searchText.trim() || undefined, sort, filterTypes: visibleTypes, diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx new file mode 100644 index 00000000000..90a008b41b0 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -0,0 +1,696 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { AnimatePresence, motion } from 'framer-motion' +import { last } from 'lodash' +import { ExternalLink, FileText, MessageCircleMore, Plus, WandSparkles } from 'lucide-react' +import Link from 'next/link' +import { useEffect, useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' + +import type { Message as MessageType } from 'ai/react' +import { useChat } from 'ai/react' +import { useParams } from 'common' +import OptInToOpenAIToggle from 'components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle' +import { MessageWithDebug } from 'components/interfaces/SQLEditor/AiAssistantPanel' +import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' +import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' +import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' +import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' +import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { BASE_PATH, IS_PLATFORM, OPT_IN_TAGS } from 'lib/constants' +import { useAppStateSnapshot } from 'state/app-state' +import { + AiIconAnimation, + Alert_Shadcn_, + AlertDescription_Shadcn_, + Button, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, + SheetHeader, + SheetSection, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' +import { Admonition, AssistantChatForm } from 'ui-patterns' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ASSISTANT_SUPPORT_ENTITIES } from './AiAssistant.constants' +import { SupportedAssistantEntities, SupportedAssistantQuickPromptTypes } from './AIAssistant.types' +import { generatePrompt, retrieveDocsUrl } from './AIAssistant.utils' +import { ContextBadge } from './ContextBadge' +import { EntitiesDropdownMenu } from './EntitiesDropdownMenu' +import { Message } from './Message' +import { SchemasDropdownMenu } from './SchemasDropdownMenu' + +const ANIMATION_DURATION = 0.3 + +interface AIAssistantProps { + id: string + className?: string + debugThread: MessageWithDebug[] + onDiff: ({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => void + onResetConversation: () => void +} + +// [Joshen] For some reason I'm having issues working with dropdown menu and scroll area + +export const AIAssistant = ({ + id, + className, + debugThread, + onDiff, + onResetConversation, +}: AIAssistantProps) => { + const { ref } = useParams() + const project = useSelectedProject() + const isOptedInToAI = useOrgOptedIntoAi() + const selectedOrganization = useSelectedOrganization() + const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM + + const { aiAssistantPanel } = useAppStateSnapshot() + const { editor } = aiAssistantPanel + + const inputRef = useRef(null) + const bottomRef = useRef(null) + + const [value, setValue] = useState('') + const [selectedDatabaseEntity, setSelectedDatabaseEntity] = useState< + SupportedAssistantEntities | '' + >('') + const [selectedSchemas, setSelectedSchemas] = useSchemasForAi(project?.ref!) + const [selectedTables, setSelectedTables] = useState<{ schema: string; name: string }[]>([]) + const [contextHistory, setContextHistory] = useState<{ + [key: string]: { entity: string; schemas: string[]; tables: string[] } + }>({}) + // [Joshen] Mainly for error handling on useChat - cause last sent messages will be voided + const [assistantError, setAssistantError] = useState() + const [lastSentMessage, setLastSentMessage] = useState() + const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false) + + const docsUrl = retrieveDocsUrl(selectedDatabaseEntity as SupportedAssistantEntities) + const entityContext = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === selectedDatabaseEntity) + const noContextAdded = + selectedDatabaseEntity.length === 0 && + selectedSchemas.length === 0 && + selectedTables.length === 0 + + const { data } = useEntityDefinitionsQuery( + { + schemas: selectedSchemas, + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { enabled: includeSchemaMetadata } + ) + + const entityDefinitions = includeSchemaMetadata + ? selectedTables.length === 0 + ? data?.map((def) => def.sql.trim()) + : data + ?.filter((def) => { + return selectedTables.some((table) => { + return def.sql.startsWith(`CREATE TABLE ${table.schema}.${table.name}`) + }) + }) + .map((def) => def.sql.trim()) + : undefined + + const { + messages: chatMessages, + isLoading: isChatLoading, + append, + } = useChat({ + id, + api: `${BASE_PATH}/api/ai/sql/generate-v2`, + body: { entityDefinitions, context: selectedDatabaseEntity }, + onError: (error) => setAssistantError(JSON.parse(error.message).error), + }) + + const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { mutate: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation() + + const { isLoading: isDebugSqlLoading } = useSqlDebugMutation() + const isLoading = isChatLoading || isDebugSqlLoading + + const messages = useMemo(() => { + const merged = [ + ...debugThread, + ...chatMessages.map((m) => ({ ...m, isDebug: false })), + ...(assistantError !== undefined && lastSentMessage !== undefined ? [lastSentMessage] : []), + ] + + return merged.sort( + (a, b) => + (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0) || + a.role.localeCompare(b.role) + ) + }, [chatMessages, debugThread, assistantError, lastSentMessage]) + + const hasMessages = messages.length > 0 + const lastMessage = last(messages) + const pendingChatReply = isLoading && lastMessage?.role === 'user' + const pendingDebugReply = + (lastMessage as MessageWithDebug)?.isDebug && + lastMessage?.role === 'assistant' && + lastMessage.content === 'Thinking...' + const pendingReply = pendingChatReply || pendingDebugReply + + const sendMessageToAssistant = (content: string) => { + const payload = { role: 'user', createdAt: new Date(), content } as MessageType + append(payload) + setAssistantError(undefined) + setLastSentMessage(payload) + } + + const toggleSchema = (schema: string) => { + if (selectedSchemas.includes(schema)) { + setSelectedSchemas(selectedSchemas.filter((s) => s !== schema)) + } else { + const newSelectedSchemas = [...selectedSchemas, schema].sort((a, b) => a.localeCompare(b)) + setSelectedSchemas(newSelectedSchemas) + } + } + + const toggleEntity = ({ schema, name }: { schema: string; name: string }) => { + const isExisting = selectedTables.find((x) => x.schema === schema && x.name === name) + if (isExisting) { + setSelectedTables(selectedTables.filter((x) => !(x.schema === schema && x.name === name))) + } else { + const newselectedTables = [...selectedTables, { schema, name }].sort( + (a, b) => a.schema.localeCompare(b.schema) || a.name.localeCompare(b.name) + ) + setSelectedTables(newselectedTables) + } + } + + const onClickQuickPrompt = (type: SupportedAssistantQuickPromptTypes) => { + const prompt = generatePrompt({ + type, + context: selectedDatabaseEntity as any, + schemas: selectedSchemas, + tables: selectedTables, + }) + if (prompt) { + setValue(prompt) + sendMessageToAssistant(prompt) + } + } + + const confirmOptInToShareSchemaData = async () => { + if (!canUpdateOrganization) { + return toast.error('You do not have the required permissions to update this organization') + } + + if (!selectedOrganization?.slug) return console.error('Organization slug is required') + + const existingOptInTags = selectedOrganization?.opt_in_tags ?? [] + + const updatedOptInTags = existingOptInTags.includes(OPT_IN_TAGS.AI_SQL) + ? existingOptInTags + : [...existingOptInTags, OPT_IN_TAGS.AI_SQL] + + updateOrganization( + { slug: selectedOrganization?.slug, opt_in_tags: updatedOptInTags }, + { + onSuccess: () => { + toast.success('Successfully opted-in') + setIsConfirmOptInModalOpen(false) + }, + } + ) + } + + useEffect(() => { + if (editor) { + const mode = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === editor) + if (mode) setSelectedDatabaseEntity(mode.id) + } + }, [editor]) + + useEffect(() => { + if (!isLoading) { + setValue('') + if (inputRef.current) inputRef.current.focus() + } + + setTimeout( + () => { + if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: 'smooth' }) + }, + isLoading ? 100 : 500 + ) + }, [isLoading]) + + useEffect(() => { + if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + useEffect(() => { + const lastMessage = last(messages) + if (lastMessage?.role === 'user') { + const entity = entityContext?.label ?? selectedDatabaseEntity + setContextHistory({ + ...contextHistory, + [lastMessage.id]: { + entity, + schemas: selectedSchemas, + tables: selectedTables.map((x) => + selectedSchemas.length > 1 ? `${x.schema}.${x.name}` : x.name + ), + }, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messages.length]) + + return ( + <> +
+ +
+ +

Assistant

+
+ + {hasMessages && ( + + + + )} + +
+ + + {hasMessages && ( + + {messages.map((m, index) => { + const isFirstUserMessage = + m.role === 'user' && messages.slice(0, index).every((msg) => msg.role !== 'user') + + return ( + onDiff({ id: m.id, diffType, sql })} + > + {isFirstUserMessage && !includeSchemaMetadata && ( + + + + )} + {isFirstUserMessage && + includeSchemaMetadata && + selectedSchemas.length === 0 && ( + + )} + + ) + })} + {assistantError !== undefined && ( + + + + )} + {!isLoading && !pendingReply && assistantError === undefined && ( +

+ Please verify all responses as the Assistant can make mistakes +

+ )} + {pendingChatReply && ( + + )} +
+ + )} + +
+ + {!hasMessages && ( + +

+ How can I help you + {!!entityContext ? ( + <> + {' '} + with{' '} + + {entityContext.id === 'rls-policies' + ? entityContext.label + : `Database ${entityContext.label}`} + + + ) : ( + ' today' + )} + ? +

+
+ )} +
+
+
+
+ + + + + + + + Add context for the assistant + + + + + + Improve the output quality of the assistant by giving it context about what + you need help with + + + {editor === null && ( + + +
+

Database Entity

+

+ Inform about what you're working with +

+
+
+ + + setSelectedDatabaseEntity(value as SupportedAssistantEntities) + } + > + {ASSISTANT_SUPPORT_ENTITIES.map((x) => ( + + {x.label} + + ))} + + +
+ )} + {includeSchemaMetadata && ( + <> + + +
+

Schemas

+

+ Share table definitions in the selected schemas +

+
+
+ + + +
+ {selectedSchemas.length > 0 && ( + + +
+

Tables

+

+ Select specific tables to share definitions for +

+
+
+ + + +
+ )} + + )} +
+
+ + {!!entityContext && ( + setSelectedDatabaseEntity('') : undefined} + /> + )} + + {selectedSchemas.length > 0 && ( + 2 ? ` and ${selectedSchemas.length - 2} other${selectedSchemas.length > 3 ? 's' : ''}` : ''}`} + onRemove={() => { + setSelectedSchemas([]) + setSelectedTables([]) + }} + tooltip={ + selectedSchemas.length > 2 ? ( + <> +

+ {selectedSchemas.length} schemas selected: +

+
    + {selectedSchemas.map((x) => ( +
  • {x}
  • + ))} +
+ + ) : undefined + } + /> + )} + + {selectedTables.length > 0 && ( + x.name) + .join( + ', ' + )}${selectedTables.length > 2 ? ` and ${selectedTables.length - 2} other${selectedTables.length > 3 ? 's' : ''}` : ''}`} + onRemove={() => setSelectedTables([])} + tooltip={ + selectedTables.length > 2 ? ( + <> +

+ {selectedTables.length} tables selected: +

+
    + {selectedTables.map((x) => ( +
  • + {x.schema}.{x.name} +
  • + ))} +
+ + ) : undefined + } + /> + )} +
+ + textarea]:rounded-none [&>textarea]:border-0 [&>textarea]:!outline-none [&>textarea]:!ring-offset-0 [&>textarea]:!ring-0' + )} + loading={isLoading} + disabled={isLoading} + placeholder={ + hasMessages ? 'Reply to the assistant...' : 'How can we help you today?' + } + value={value} + onValueChange={(e) => setValue(e.target.value)} + onSubmit={(event) => { + event.preventDefault() + sendMessageToAssistant(value) + }} + /> +
+ + {!hasMessages && ( + +
+ + + + + + Suggest some{' '} + {entityContext?.id === 'rls-policies' + ? entityContext.label + : `database ${entityContext?.label.toLowerCase()}`} + + + + + + + + Provide some examples of{' '} + {entityContext?.id === 'rls-policies' + ? entityContext.label + : `database ${entityContext?.label.toLowerCase()}`} + + + + + + + + What are{' '} + {entityContext?.id === 'rls-policies' + ? entityContext.label + : `database ${entityContext?.label.toLowerCase()}`} + ? + + + {docsUrl !== undefined && ( + + )} +
+
+ )} +
+
+
+ +
+ setIsConfirmOptInModalOpen(false)} + onConfirm={confirmOptInToShareSchemaData} + loading={isUpdating} + > +

+ By opting into sending anonymous data, Supabase AI can improve the answers it shows you. + This is an organization-wide setting, and affects all projects in your organization. +

+ + +
+ + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts new file mode 100644 index 00000000000..38fb580b306 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.types.ts @@ -0,0 +1,2 @@ +export type SupportedAssistantEntities = 'rls-policies' | 'functions' +export type SupportedAssistantQuickPromptTypes = 'suggest' | 'examples' | 'ask' diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts new file mode 100644 index 00000000000..b0b28d12550 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -0,0 +1,125 @@ +import { SupportedAssistantEntities, SupportedAssistantQuickPromptTypes } from './AIAssistant.types' + +const PLACEHOLDER_PREFIX = `-- Press tab to use this code +\n \n` + +const PLACEHOLDER_LIMIT = `Just three examples will do.` + +export const generateTitle = (editor?: SupportedAssistantEntities | null) => { + switch (editor) { + case 'functions': + return 'Create a new function' + case 'rls-policies': + return 'Create a new RLS policy' + default: + return 'SQL Scratch Pad' + } +} + +export const generateCTA = (editor?: SupportedAssistantEntities | null) => { + switch (editor) { + case 'functions': + return 'Save function' + case 'rls-policies': + return 'Save policy' + default: + return 'Run query' + } +} + +export const generatePlaceholder = (editor?: SupportedAssistantEntities | null) => { + switch (editor) { + case 'functions': + return `${PLACEHOLDER_PREFIX} +CREATE FUNCTION *schema*.*function_name*(*param1 type*, *param2 type*)\n +  RETURNS *return_type*\n +  LANGUAGE *plpgsql*\n +  SECURITY DEFINER\n +  SET *search_path = ''*\n +AS $$\n +DECLARE\n +  *-- Variable declarations*\n +BEGIN\n +  *-- Function logic*\n +END;\n +$$; +` + case 'rls-policies': + return `${PLACEHOLDER_PREFIX} +CREATE POLICY *name* ON *table_name*\n +AS PERMISSIVE -- PERMISSIVE | RESTRICTIVE\n +FOR ALL -- ALL | SELECT | INSERT | UPDATE | DELETE\n +TO *role_name* -- Default: public\n +USING ( *using_expression* )\n +WITH CHECK ( *check_expression* ); +` + default: + return undefined + } +} + +export const retrieveDocsUrl = (editor?: SupportedAssistantEntities | null) => { + switch (editor) { + case 'functions': + return 'https://supabase.com/docs/guides/database/functions' + case 'rls-policies': + return 'https://supabase.com/docs/guides/database/postgres/row-level-security' + default: + return undefined + } +} + +// [Joshen] This is just very basic validation, but possible can extend perhaps +export const validateQuery = (editor: SupportedAssistantEntities | null, query: string) => { + const formattedQuery = query.toLowerCase().replaceAll('\n', ' ') + + switch (editor) { + case 'functions': + return ( + formattedQuery.includes('create function') || + formattedQuery.includes('create or replace function') + ) + case 'rls-policies': + return formattedQuery.includes('create policy') + default: + return true + } +} + +export const generatePrompt = ({ + type, + context, + schemas, + tables, +}: { + type: SupportedAssistantQuickPromptTypes + context: SupportedAssistantEntities + schemas: string[] + tables: { schema: string; name: string }[] +}) => { + if (type === 'examples') { + return `What are some common examples of user-defined database ${context}? ${PLACEHOLDER_LIMIT}` + } else if (type === 'ask') { + return `Could you explain to me what are used-defined database ${context}?` + } else if (type === 'suggest') { + const output = + context === 'functions' + ? 'user-defined database functions' + : context === 'rls-policies' + ? 'RLS policies' + : '' + + const suffix = + context === 'functions' ? 'Let me know for which tables each function will be useful' : '' + + const basePrompt = `Suggest some ${output} that might be useful` + + if (tables.length > 0 && schemas.length > 0) { + return `${basePrompt} for the following tables within this database: ${tables.map((x) => `${x.schema}.${x.name}`)}. ${PLACEHOLDER_LIMIT} ${suffix}`.trim() + } else if (schemas.length > 0) { + return `${basePrompt} for the tables in the following schemas within this database: ${schemas.join(', ')}. ${suffix}`.trim() + } + + return basePrompt + } +} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx new file mode 100644 index 00000000000..c2d0379154a --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx @@ -0,0 +1,439 @@ +import { useQueryClient } from '@tanstack/react-query' +import { uniqBy } from 'lodash' +import { Command, CornerDownLeft, Loader2, X } from 'lucide-react' +import { useRouter } from 'next/router' +import { useCallback, useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' + +import { useChat } from 'ai/react' +import { useParams } from 'common' +import { generateThreadMessage } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils' +import { MessageWithDebug } from 'components/interfaces/SQLEditor/AiAssistantPanel' +import { sqlAiDisclaimerComment } from 'components/interfaces/SQLEditor/SQLEditor.constants' +import { DiffType, IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types' +import { suffixWithLimit } from 'components/interfaces/SQLEditor/SQLEditor.utils' +import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' +import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' +import { databasePoliciesKeys } from 'data/database-policies/keys' +import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { sqlKeys } from 'data/sql/keys' +import { usePrevious } from 'hooks/deprecated' +import { useLocalStorage } from 'hooks/misc/useLocalStorage' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { SqlEditor } from 'icons' +import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { detectOS, uuidv4 } from 'lib/helpers' +import { useAppStateSnapshot } from 'state/app-state' +import { + Button, + cn, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' +import { Admonition } from 'ui-patterns' +import CodeEditor from '../CodeEditor/CodeEditor' +import { AIAssistant } from './AIAssistant' +import { generateCTA, generatePlaceholder, generateTitle, validateQuery } from './AIAssistant.utils' +import { ASSISTANT_SUPPORT_ENTITIES } from './AiAssistant.constants' + +export const AiAssistantPanel = () => { + const os = detectOS() + const router = useRouter() + const { ref } = useParams() + const project = useSelectedProject() + const queryClient = useQueryClient() + const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot() + + const { open, editor, content } = aiAssistantPanel + const previousEditor = usePrevious(editor) + + const [isAcknowledged, setIsAcknowledged] = useLocalStorage( + LOCAL_STORAGE_KEYS.SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED, + false + ) + + const [chatId, setChatId] = useState(() => uuidv4()) + const [error, setError] = useState() + const [results, setResults] = useState(undefined) + const [showResults, setShowResults] = useState(false) + const [showWarning, setShowWarning] = useState(false) + const [debugThread, setDebugThread] = useState([]) + + // [Joshen] JFYI I'm opting to just have the assistant always show and not toggle-able + // I don't really see any negatives of keeping it open (or benefits from hiding) tbh + // const [showAssistant, setShowAssistant] = useState(true) + const title = generateTitle(editor) + const placeholder = generatePlaceholder(editor) + const ctaText = generateCTA(editor) + const editorRef = useRef() + const editorModel = editorRef.current?.getModel() + + const numResults = (results ?? []).length + const [errorHeader, ...errorContent] = + (error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? [] + const entityContext = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === editor) + + const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({ + onSuccess: async (res) => { + // [Joshen] If in a specific editor context mode, assume that intent was to create/update + // a database entity - so close it once success. Otherwise it's in Quick SQL mode and we + // show the results. Currently though it assumes we're "creating", thinking need to support "updating" too + if (editor !== null) { + switch (editor) { + case 'functions': + await queryClient.invalidateQueries(sqlKeys.query(ref, ['functions-list'])) + break + case 'rls-policies': + await queryClient.invalidateQueries(databasePoliciesKeys.list(ref)) + break + } + + toast.success(`Successfully created ${entityContext?.name}!`) + setAiAssistantPanel({ open: false }) + } else { + setShowResults(true) + setResults(res.result) + } + }, + onError: (error) => { + setError(error) + setResults([]) + }, + }) + + const { append } = useChat({ + id: chatId, + api: `${BASE_PATH}/api/ai/sql/generate-v2`, + // [Joshen] Don't need entity definitions if calling here cause support action + // via AI here is just to explain code segment, no need for context + body: {}, + }) + + const { mutateAsync: debugSql } = useSqlDebugMutation({ onError: () => {} }) + + const onExecuteSql = (skipValidation = false) => { + setError(undefined) + setShowWarning(false) + + const query = editorRef.current?.getValue() ?? '' + if (query.length === 0) return + + if (editor !== undefined && !skipValidation) { + // [Joshen] Some basic validation logic + const validated = validateQuery(editor, query) + if (!validated) { + return setShowWarning(true) + } + } + + executeSql({ + sql: suffixWithLimit(query, 100), + projectRef: project?.ref, + connectionString: project?.connectionString, + handleError: (error) => { + throw error + }, + }) + } + + const onFixWithAssistant = async () => { + if (!error) return + + const messageId = uuidv4() + const query = editorRef.current?.getValue() ?? '' + + const assistantMessageBefore = generateThreadMessage({ + id: messageId, + content: 'Thinking...', + isDebug: true, + }) + + setDebugThread([...debugThread, assistantMessageBefore]) + + try { + const { solution, sql } = await debugSql({ + sql: query, + errorMessage: error.message, + }) + + const assistantMessageAfter = generateThreadMessage({ + id: messageId, + content: `${solution}\n\`\`\`sql\n${sql}\n\`\`\``, + isDebug: true, + }) + const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id) + setDebugThread(cleanedMessages) + } catch (error) { + const assistantMessageAfter = generateThreadMessage({ + id: messageId, + content: `Hmm, Sorry but I'm unable to find a solution for the error that you're facing`, + isDebug: true, + }) + const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id) + setDebugThread(cleanedMessages) + } + } + + const onExplainSql = (value: string) => { + append({ + role: 'user', + createdAt: new Date(), + content: ` + Can you explain this section to me in more detail?\n + ${value} + `.trim(), + }) + } + + const updateEditorWithCheckForDiff = useCallback( + ({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => { + if (!editorModel) return + + const existingValue = editorRef.current?.getValue() ?? '' + if (existingValue.length === 0) { + // if the editor is empty, just copy over the code + editorRef.current?.executeEdits('apply-ai-message', [ + { + text: `${sqlAiDisclaimerComment}\n\n${sql}`, + range: editorModel.getFullModelRange(), + }, + ]) + } else { + const currentSql = editorRef.current?.getValue() + editorRef.current?.executeEdits('apply-ai-message', [ + { + text: `${currentSql}\n\n${sqlAiDisclaimerComment}\n\n${sql}`, + range: editorModel.getFullModelRange(), + }, + ]) + } + }, + [editorModel] + ) + + useEffect(() => { + if (open) { + // [Joshen] Only reset the assistant if the editor changed + if (previousEditor !== editor) { + setChatId(uuidv4()) + setError(undefined) + setShowWarning(false) + setDebugThread([]) + setResults(undefined) + setShowResults(false) + } else { + // [Joshen] Repopulate the code editor with the content from where the user left off + // setTimeout is just to give time for the editorRef to get associated + setTimeout(() => { + const editorModel = editorRef.current?.getModel() + if (content && editorModel) { + editorRef.current?.executeEdits('apply-ai-message', [ + { text: content, range: editorModel.getFullModelRange() }, + ]) + } + }, 100) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + + // [Joshen] Just to test the concept of a universal assistant of sorts + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.code === 'KeyI') { + setAiAssistantPanel({ open: true, editor: null }) + } + } + if (project !== undefined) window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project]) + + // [Joshen] Whenever the deps change recalculate the height of the editor + useEffect(() => { + editorRef.current?.layout({ width: 0, height: 0 }) + window.requestAnimationFrame(() => editorRef.current?.layout()) + }, [error, showWarning, isExecuting, numResults, showResults]) + + return ( + { + if (open) { + // [Joshen] Save the code content when closing - should allow users to continue from + // where they left off IF they are opening the assistant again in the same "editor" context + const existingValue = editorRef.current?.getValue() ?? '' + setAiAssistantPanel({ open: false, content: existingValue }) + } else { + setAiAssistantPanel({ open: true }) + } + }} + > + + {/* Assistant */} + setChatId(uuidv4())} + /> + + {/* Editor */} +
+ + {title} + + + + + } + /> + )} + {showWarning && ( + +

Are you sure you want to run this query?

+ + + } + /> + )} + {results !== undefined && results.length > 0 && ( + <> +
+ +
+
+

+ {results.length} rows + {results.length >= 100 && ` (Limited to only 100 rows)`} +

+ +
+ + )} + {results !== undefined && results.length === 0 && ( +
+

Success. No rows returned.

+
+ )} + + + + +
+
+
+ + + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts b/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts new file mode 100644 index 00000000000..e9bc5bdeb55 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts @@ -0,0 +1,10 @@ +import { SupportedAssistantEntities } from './AIAssistant.types' + +export const ASSISTANT_SUPPORT_ENTITIES: { + id: SupportedAssistantEntities + label: string + name: string +}[] = [ + { id: 'rls-policies', label: 'RLS Policies', name: 'RLS policy' }, + { id: 'functions', label: 'Functions', name: 'database function' }, +] diff --git a/apps/studio/components/ui/AIAssistantPanel/ContextBadge.tsx b/apps/studio/components/ui/AIAssistantPanel/ContextBadge.tsx new file mode 100644 index 00000000000..ba205b176e0 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/ContextBadge.tsx @@ -0,0 +1,33 @@ +import { X } from 'lucide-react' +import { ReactNode } from 'react' +import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' + +interface ContextBadgeProps { + label: string + value: string + tooltip?: ReactNode + onRemove?: () => void +} + +export const ContextBadge = ({ label, value, tooltip, onRemove }: ContextBadgeProps) => { + return ( + + +
+
+ {label} + {value} +
+ {onRemove !== undefined && ( + onRemove()} + /> + )} +
+
+ {tooltip !== undefined && {tooltip}} +
+ ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/DropdownScrollArea.tsx b/apps/studio/components/ui/AIAssistantPanel/DropdownScrollArea.tsx new file mode 100644 index 00000000000..1bbe09c0190 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/DropdownScrollArea.tsx @@ -0,0 +1,36 @@ +import { PropsWithChildren, useEffect } from 'react' +import { ScrollArea } from 'ui' + +interface DropdownScrollAreaProps { + id?: string + className?: string +} + +// [Joshen] This is a temporary solution to a really odd behavior i've observed with the components in Shadcn +// Specifically if we're using a ScrollArea (or even just anything with overflow) within a DropdownMenu, within a Sheet +// The mouse wheel event to scroll in the ScrollArea is somehow getting ignored in this specific context, hence the fix here + +export const DropdownScrollArea = ({ + id, + className, + children, +}: PropsWithChildren) => { + useEffect(() => { + const scrollFix = (e: WheelEvent) => e.stopPropagation() + + if (id) { + const el = document.getElementById(id) + if (el) { + el.addEventListener('wheel', scrollFix, { passive: false }) + return () => el.removeEventListener('wheel', scrollFix) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + {children} + + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/EntitiesDropdownMenu.tsx b/apps/studio/components/ui/AIAssistantPanel/EntitiesDropdownMenu.tsx new file mode 100644 index 00000000000..d301fb5814e --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/EntitiesDropdownMenu.tsx @@ -0,0 +1,110 @@ +import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { debounce } from 'lodash' +import { Check, Loader2, Search } from 'lucide-react' +import { useCallback, useState } from 'react' +import { + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + ScrollArea, +} from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { DropdownScrollArea } from './DropdownScrollArea' +import { Admonition } from 'ui-patterns' + +interface EntitiesDropdownMenuProps { + selectedSchemas: string[] + selectedEntities: { schema: string; name: string }[] + onToggleEntity: (value: { schema: string; name: string }) => void +} + +export const EntitiesDropdownMenu = ({ + selectedSchemas, + selectedEntities, + onToggleEntity, +}: EntitiesDropdownMenuProps) => { + const project = useSelectedProject() + const [search, setSearch] = useState('') + + // [Joshen] Not gonna do infinite loading here, but rather just try to load sufficient tables + // I reckon users will opt to just search if there's many tables here given the small real estate + const { + data: entitiesData, + error, + isLoading, + isFetching, + isError, + } = useEntityTypesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schemas: selectedSchemas, + limit: 100, + search: search.length === 0 ? undefined : search, + }) + const entities = ( + entitiesData?.pages[0].data.entities ? [...entitiesData?.pages[0].data.entities] : [] + ).sort((a, b) => (a.name > b.name ? 0 : -1)) + + // eslint-disable-next-line react-hooks/exhaustive-deps + const searchInputHandler = useCallback(debounce(setSearch, 1000), []) + + if (isError) { + return ( + + ) + } + + return ( + + + {/* Separate input for async searching */} + } + className="text-xs outline-none rounded-b-none border-0 border-b pl-9 pr-7" + onChange={(e) => searchInputHandler(e.target.value)} + actions={ + (isLoading || isFetching) && ( + + ) + } + /> + No tables found + + 7 ? 'h-[210px]' : ''} + > + {entities?.map((entity) => ( + onToggleEntity({ schema: entity.schema, name: entity.name })} + onClick={() => onToggleEntity({ schema: entity.schema, name: entity.name })} + > + + {entity.schema} + {entity.name} + + {selectedEntities.find( + (x) => x.name === entity.name && x.schema === entity.schema + ) && } + + ))} + + + + + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx new file mode 100644 index 00000000000..3d20ff3c6c6 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx @@ -0,0 +1,146 @@ +import dayjs from 'dayjs' +import { noop } from 'lodash' +import Image from 'next/image' +import { PropsWithChildren, memo, useMemo } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { AiIconAnimation, Badge, cn, markdownComponents, WarningIcon } from 'ui' + +import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' +import { useProfile } from 'lib/profile' +import { MessagePre } from './MessagePre' + +interface MessageProps { + name?: string + role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool' + content?: string + createdAt?: number + isDebug?: boolean + isSelected?: boolean + action?: React.ReactNode + context?: { entity: string; schemas: string[]; tables: string[] } + onDiff?: (type: DiffType, s: string) => void + variant?: 'default' | 'warning' +} + +export const Message = memo(function Message({ + name, + role, + content, + createdAt, + isDebug, + isSelected = false, + context, + children, + action = null, + variant = 'default', + onDiff = noop, +}: PropsWithChildren) { + const { profile } = useProfile() + const isUser = role === 'user' + + const icon = useMemo(() => { + return role === 'assistant' ? ( + + ) : ( +
+ avatar +
+ ) + }, [content, profile?.username, role]) + + const formattedContext = + context !== undefined + ? Object.entries(context) + .filter(([_, value]) => value.length > 0) + .map(([key, value]) => { + return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${Array.isArray(value) ? value.join(', ') : value}` + }) + .join(' • ') + : undefined + + if (!content) return null + + return ( +
+
+
+ {variant === 'warning' ? : icon} + +
+
+ {!isUser ? 'Assistant' : name ? name : 'You'} + {createdAt && ( + + {dayjs(createdAt).fromNow()} + + )} +
+ {role === 'user' && context !== undefined && ( + {formattedContext} + )} +
+ + {isDebug && Debug request} +
{' '} + {action} +
+ + { + return ( + div>pre]:max-w-full', + isSelected ? '[&>div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : '' + )} + > + {props.children[0].props.children} + + ) + }, + ol: (props: any) => { + return
    {props.children}
+ }, + li: (props: any) => { + return
  • {props.children}
  • + }, + h3: (props: any) => { + return

    {props.children}

    + }, + code: (props: any) => { + return {props.children} + }, + }} + > + {content} +
    + {children} +
    + ) +}) diff --git a/apps/studio/components/ui/AIAssistantPanel/MessagePre.tsx b/apps/studio/components/ui/AIAssistantPanel/MessagePre.tsx new file mode 100644 index 00000000000..414b5fc0917 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/MessagePre.tsx @@ -0,0 +1,113 @@ +import { InsertCode, ReplaceCode } from 'icons' +import { Check, Copy } from 'lucide-react' +import { useEffect, useState } from 'react' +import { format } from 'sql-formatter' +import { + Button, + CodeBlock, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, + Tooltip_Shadcn_, + cn, +} from 'ui' + +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' + +interface MessagePreProps { + onDiff: (type: DiffType, s: string) => void + children: string[] + className?: string +} + +export const MessagePre = ({ onDiff, children, className }: MessagePreProps) => { + const [copied, setCopied] = useState(false) + + const { mutate: sendEvent } = useSendEventMutation() + + useEffect(() => { + if (!copied) return + const timer = setTimeout(() => setCopied(false), 2000) + return () => clearTimeout(timer) + }, [copied]) + + let formatted = (children || [''])[0] + try { + formatted = format(formatted, { language: 'postgresql', keywordCase: 'upper' }) + } catch {} + + if (formatted.length === 0) { + return null + } + + function handleCopy(formatted: string) { + navigator.clipboard.writeText(formatted).then() + setCopied(true) + } + + return ( +
    +      code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block'
    +        )}
    +        hideCopy
    +        hideLineNumbers
    +      />
    +      
    + + + + + + Insert code + + + + + + + + + Copy code + + +
    +
    + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/SchemasDropdownMenu.tsx b/apps/studio/components/ui/AIAssistantPanel/SchemasDropdownMenu.tsx new file mode 100644 index 00000000000..6db2472077c --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/SchemasDropdownMenu.tsx @@ -0,0 +1,61 @@ +import { + cn, + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, +} from 'ui' +import { DropdownScrollArea } from './DropdownScrollArea' +import { Check } from 'lucide-react' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' + +interface SchemasDropdownMenuProps { + selectedSchemas: string[] + onToggleSchema: (schema: string) => void +} + +export const SchemasDropdownMenu = ({ + selectedSchemas, + onToggleSchema, +}: SchemasDropdownMenuProps) => { + const project = useSelectedProject() + + const { data: schemaDatas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const schemas = (schemaDatas || []).sort((a, b) => a.name.localeCompare(b.name)) + + return ( + + + + No schemas found + + 7 ? 'h-[210px]' : '')} + > + {schemas?.map((schema) => ( + onToggleSchema(schema.name)} + onClick={() => onToggleSchema(schema.name)} + > + {schema.name} + {selectedSchemas.includes(schema.name) && ( + + )} + + ))} + + + + + ) +} diff --git a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx index 24eeb0d677d..62c08d73b6b 100644 --- a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -1,13 +1,23 @@ -import Editor, { EditorProps, Monaco, OnChange, OnMount } from '@monaco-editor/react' +import Editor, { EditorProps, Monaco, OnChange, OnMount, useMonaco } from '@monaco-editor/react' import { merge, noop } from 'lodash' import { editor } from 'monaco-editor' -import { useEffect, useRef, useState } from 'react' +import { MutableRefObject, useEffect, useRef, useState } from 'react' import { Markdown } from 'components/interfaces/Markdown' import { timeout } from 'lib/helpers' import { cn } from 'ui' import { Loading } from '../Loading' import { alignEditor } from './CodeEditor.utils' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { formatQuery } from 'data/sql/format-sql-query' + +type CodeEditorActions = { enabled: boolean; callback: (value: any) => void } +const DEFAULT_ACTIONS = { + runQuery: { enabled: false, callback: noop }, + explainCode: { enabled: false, callback: noop }, + formatDocument: { enabled: true, callback: noop }, + placeholderFill: { enabled: true }, +} interface CodeEditorProps { id: string @@ -15,15 +25,21 @@ interface CodeEditorProps { autofocus?: boolean defaultValue?: string isReadOnly?: boolean - onInputChange?: (value?: string) => void - onInputRun?: (value: string) => void hideLineNumbers?: boolean className?: string loading?: boolean options?: EditorProps['options'] value?: string placeholder?: string - disableTabToUsePlaceholder?: boolean + /* Determines what actions to add for code editor context menu */ + actions?: Partial<{ + runQuery: CodeEditorActions + formatDocument: CodeEditorActions + placeholderFill: Omit + explainCode: CodeEditorActions + }> + editorRef?: MutableRefObject + onInputChange?: (value?: string) => void } const CodeEditor = ({ @@ -33,83 +49,31 @@ const CodeEditor = ({ autofocus = true, isReadOnly = false, hideLineNumbers = false, - onInputChange = noop, - onInputRun = noop, className, loading, options, value, placeholder, - disableTabToUsePlaceholder = false, + actions = DEFAULT_ACTIONS, + editorRef: editorRefProps, + onInputChange = noop, }: CodeEditorProps) => { - const hasValue = useRef() + const monaco = useMonaco() + const project = useSelectedProject() - const editorRef = useRef() + const hasValue = useRef() + const ref = useRef() + const editorRef = editorRefProps || ref const monacoRef = useRef() + const { runQuery, placeholderFill, formatDocument, explainCode } = { + ...DEFAULT_ACTIONS, + ...actions, + } + const showPlaceholderDefault = placeholder !== undefined && (value ?? '').trim().length === 0 const [showPlaceholder, setShowPlaceholder] = useState(showPlaceholderDefault) - const onMount: OnMount = async (editor, monaco) => { - editorRef.current = editor - monacoRef.current = monaco - alignEditor(editor) - - hasValue.current = editor.createContextKey('hasValue', false) - hasValue.current.set(value !== undefined && value.trim().length > 0) - setShowPlaceholder(showPlaceholderDefault) - - if (!disableTabToUsePlaceholder) { - editor.addCommand( - monaco.KeyCode.Tab, - () => { - editor.executeEdits('source', [ - { - // @ts-ignore - identifier: 'add-placeholder', - range: new monaco.Range(1, 1, 1, 1), - text: (placeholder ?? '') - .split('\n\n') - .join('\n') - .replaceAll('*', '') - .replaceAll(' ', ''), - }, - ]) - }, - '!hasValue' - ) - } - - editor.addAction({ - id: 'supabase', - label: 'Run Query', - keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter], - contextMenuGroupId: 'operation', - contextMenuOrder: 0, - run: async () => { - const selectedValue = (editorRef?.current as any) - .getModel() - .getValueInRange((editorRef?.current as any)?.getSelection()) - onInputRun(selectedValue || (editorRef?.current as any)?.getValue()) - }, - }) - - const model = editor.getModel() - if (model) { - const position = model.getPositionAt((value ?? '').length) - editor.setPosition(position) - } - - await timeout(500) - if (autofocus) editor?.focus() - } - - const onChangeContent: OnChange = (value) => { - hasValue.current.set((value ?? '').length > 0) - setShowPlaceholder(!value) - onInputChange(value) - } - const optionsMerged = merge( { tabSize: 2, @@ -128,13 +92,105 @@ const CodeEditor = ({ options ) + const formatPgsql = async (value: string) => { + try { + if (!project) throw new Error('No project') + const formatted = await formatQuery({ + projectRef: project.ref, + connectionString: project.connectionString, + sql: value, + }) + return formatted.result + } catch (error) { + console.error('formatPgsql error:', error) + return value + } + } + + const onMount: OnMount = async (editor, monaco) => { + editorRef.current = editor + monacoRef.current = monaco + alignEditor(editor) + + hasValue.current = editor.createContextKey('hasValue', false) + hasValue.current.set(value !== undefined && value.trim().length > 0) + setShowPlaceholder(showPlaceholderDefault) + + if (placeholderFill.enabled) { + editor.addCommand( + monaco.KeyCode.Tab, + () => { + editor.executeEdits('source', [ + { + // @ts-ignore + identifier: 'add-placeholder', + range: new monaco.Range(1, 1, 1, 1), + text: (placeholder ?? '') + .split('\n\n') + .join('\n') + .replaceAll('*', '') + .replaceAll(' ', ' '), + }, + ]) + }, + '!hasValue' + ) + } + + if (runQuery.enabled) { + editor.addAction({ + id: 'run-query', + label: 'Run Query', + keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter], + contextMenuGroupId: 'operation', + contextMenuOrder: 0, + run: () => { + const selectedValue = (editorRef?.current as any) + .getModel() + .getValueInRange((editorRef?.current as any)?.getSelection()) + runQuery.callback(selectedValue || (editorRef?.current as any)?.getValue()) + }, + }) + } + + if (explainCode.enabled) { + editor.addAction({ + id: 'explain-code', + label: 'Explain Code', + contextMenuGroupId: 'operation', + contextMenuOrder: 1, + run: () => { + const selectedValue = (editorRef?.current as any) + .getModel() + .getValueInRange((editorRef?.current as any)?.getSelection()) + explainCode.callback(selectedValue) + }, + }) + } + + const model = editor.getModel() + if (model) { + const position = model.getPositionAt((value ?? '').length) + editor.setPosition(position) + } + + await timeout(500) + if (autofocus) editor?.focus() + } + + const onChangeContent: OnChange = (value) => { + hasValue.current.set((value ?? '').length > 0) + setShowPlaceholder(!value) + onInputChange(value) + } + useEffect(() => { setShowPlaceholder(showPlaceholderDefault) }, [showPlaceholderDefault]) useEffect(() => { if ( - !disableTabToUsePlaceholder && + placeholderFill.enabled && editorRef.current !== undefined && monacoRef.current !== undefined ) { @@ -160,7 +216,22 @@ const CodeEditor = ({ '!hasValue' ) } - }, [placeholder, disableTabToUsePlaceholder]) + }, [placeholder, placeholderFill.enabled]) + + useEffect(() => { + if (monaco && project && formatDocument.enabled) { + const formatProvider = monaco.languages.registerDocumentFormattingEditProvider('pgsql', { + async provideDocumentFormattingEdits(model: any) { + const value = model.getValue() + const formatted = await formatPgsql(value) + formatDocument.callback(formatted) + return [{ range: model.getFullModelRange(), text: formatted }] + }, + }) + return () => formatProvider.dispose() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [monaco, project, formatDocument.enabled]) return ( <> diff --git a/apps/studio/components/ui/SchemaComboBox.tsx b/apps/studio/components/ui/SchemaComboBox.tsx index dc93562dc9c..f4810f6df7c 100644 --- a/apps/studio/components/ui/SchemaComboBox.tsx +++ b/apps/studio/components/ui/SchemaComboBox.tsx @@ -19,6 +19,7 @@ import { Popover_Shadcn_, ScrollArea, } from 'ui' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' interface SchemaComboBoxProps { className?: string @@ -44,7 +45,7 @@ export const SchemaComboBox = ({ }: SchemaComboBoxProps) => { const [open, setOpen] = useState(false) - const { project } = useProjectContext() + const project = useSelectedProject() const { data, isLoading: isSchemasLoading, diff --git a/apps/studio/data/database/entity-definitions-query.ts b/apps/studio/data/database/entity-definitions-query.ts index 0e820869fcf..e288f81fa0e 100644 --- a/apps/studio/data/database/entity-definitions-query.ts +++ b/apps/studio/data/database/entity-definitions-query.ts @@ -3,7 +3,13 @@ import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/exec import { CREATE_PG_GET_TABLEDEF_SQL } from './database-query-constants' import { databaseKeys } from './keys' -export const getEntityDefinitionsQuery = (schemas: string[], limit = 100) => { +export const getEntityDefinitionsQuery = ({ + schemas, + limit = 100, +}: { + schemas: string[] + limit?: number +}) => { const sql = /* SQL */ ` ${CREATE_PG_GET_TABLEDEF_SQL} @@ -99,7 +105,7 @@ export const useEntityDefinitionsQuery = < { projectRef, connectionString, - sql: getEntityDefinitionsQuery(schemas, limit), + sql: getEntityDefinitionsQuery({ schemas, limit }), queryKey: databaseKeys.entityDefinitions(projectRef, schemas), }, { diff --git a/apps/studio/data/entity-types/entity-types-infinite-query.ts b/apps/studio/data/entity-types/entity-types-infinite-query.ts index 470bd56933b..3931fb892e9 100644 --- a/apps/studio/data/entity-types/entity-types-infinite-query.ts +++ b/apps/studio/data/entity-types/entity-types-infinite-query.ts @@ -5,7 +5,7 @@ import { entityTypeKeys } from './keys' export type EntityTypesVariables = { projectRef?: string - schema?: string + schemas?: string[] search?: string limit?: number page?: number @@ -24,7 +24,7 @@ export async function getEntityTypes( { projectRef, connectionString, - schema = 'public', + schemas = ['public'], search, limit = 100, page = 0, @@ -67,7 +67,7 @@ export async function getEntityTypes( ) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ) - and nc.nspname = '${schema}' + and nc.nspname in (${schemas.map((x) => `'${x}'`)}) ${search ? `and c.relname ilike '%${search}%'` : ''} order by ${innerOrderBy} limit ${limit} @@ -111,7 +111,7 @@ export const useEntityTypesQuery = ( { projectRef, connectionString, - schema = 'public', + schemas = ['public'], search, limit = 100, sort, @@ -123,10 +123,19 @@ export const useEntityTypesQuery = ( }: UseInfiniteQueryOptions = {} ) => useInfiniteQuery( - entityTypeKeys.list(projectRef, { schema, search, sort, limit, filterTypes }), + entityTypeKeys.list(projectRef, { schemas, search, sort, limit, filterTypes }), ({ signal, pageParam }) => getEntityTypes( - { projectRef, connectionString, schema, search, limit, page: pageParam, sort, filterTypes }, + { + projectRef, + connectionString, + schemas, + search, + limit, + page: pageParam, + sort, + filterTypes, + }, signal ), { diff --git a/apps/studio/data/entity-types/keys.ts b/apps/studio/data/entity-types/keys.ts index c04fc7dbd12..7e3505fdffc 100644 --- a/apps/studio/data/entity-types/keys.ts +++ b/apps/studio/data/entity-types/keys.ts @@ -2,7 +2,7 @@ export const entityTypeKeys = { list: ( projectRef: string | undefined, params?: { - schema?: string + schemas?: string[] search?: string sort?: 'alphabetical' | 'grouped-alphabetical' limit?: number diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index da484ef951d..c1e359bda6f 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -36,8 +36,11 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout', UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel', UI_PREVIEW_CLS: 'supabase-ui-cls', + UI_PREVIEW_FUNCTIONS_ASSISTANT: 'supabase-ui-functions-assistant', UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown', + SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged', + DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`, SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled', diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 6c7899c806b..63fa4d1a825 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -41,6 +41,7 @@ import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu' import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal' import { GenerateSql } from 'components/interfaces/SqlGenerator/SqlGenerator' +import { AiAssistantPanel } from 'components/ui/AIAssistantPanel/AIAssistantPanel' import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState' import FlagProvider from 'components/ui/Flag/FlagProvider' import PageTelemetry from 'components/ui/PageTelemetry' @@ -147,6 +148,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { + diff --git a/apps/studio/pages/api/ai/sql/generate-v2.ts b/apps/studio/pages/api/ai/sql/generate-v2.ts index d529568da0b..d21b384a479 100644 --- a/apps/studio/pages/api/ai/sql/generate-v2.ts +++ b/apps/studio/pages/api/ai/sql/generate-v2.ts @@ -1,5 +1,6 @@ import { StreamingTextResponse } from 'ai' import { chatSql } from 'ai-commands/edge' +import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types' import { NextRequest } from 'next/server' import OpenAI from 'openai' @@ -70,12 +71,13 @@ async function handlePost(request: NextRequest) { messages: { content: string; role: 'user' | 'assistant' }[] existingSql?: string entityDefinitions: string[] + context?: SupportedAssistantEntities }>) - const { messages, existingSql, entityDefinitions } = body + const { messages, existingSql, entityDefinitions, context } = body try { - const stream = await chatSql(openai, messages, existingSql, entityDefinitions) + const stream = await chatSql(openai, messages, existingSql, entityDefinitions, context) return new StreamingTextResponse(stream) } catch (error) { if (error instanceof Error) { diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx index cb349d96b56..f2640fc356a 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -366,7 +366,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => { language="pgsql" // its bq sql but monaco doesn't have a language for it defaultValue={warehouseEditorValue} onInputChange={(v) => setWarehouseEditorValue(v || '')} - onInputRun={handleRun} + actions={{ runQuery: { enabled: true, callback: handleRun } }} /> ) : ( { language="pgsql" defaultValue={editorValue} onInputChange={(v) => setEditorValue(v || '')} - onInputRun={handleRun} + actions={{ runQuery: { enabled: true, callback: handleRun } }} /> )} diff --git a/apps/studio/public/img/previews/functions-assistant.png b/apps/studio/public/img/previews/functions-assistant.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc3f94c8f7dcd783f5d1d3908815b3626f576e9 GIT binary patch literal 275225 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%0De@(@!a7|qAhBitr2vu&c0<^S7w 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%0De(s<%(&33(;NL)VNegmr!Z2t+8VA$2C&`fC}b2 z@BKN`;6~|{a5CoZI>5CuTnR&wM7>LkIjHME{}5F&W-EaW=luoP`PJxG*gp#%gfSp? z4nAbC^OKWvGn-r@{U3+GNs0x|8BKThr}_L389*EYv=^8Rzg5MsE_T-@$CL@3u-hEf zs+Y@Mr5nlD{oAJthDHC%m=lUh{;fJq`U(^8&L%V}Av6D!@yLk?MXxuR;H08nCJ3u- z=w69$O<7>htR#0)tT__qX@7vg#+gz_y;2;URT;a^4ymg*6-FpJNSsSB>2wnARkIpY zI6j3h0=WNlUPF?;lu2;Sc~WcCa+g^+{PLUQkpgitwv0(B>n$yF#d^C`Nl>NhsH}7J z{MydT>9j##r8S;TA)kz%cQ&{>oYk2jAFF*RqqaS-|1Sf;nIs4b<~L*z#U4~Cn#5@E zjfCj+`xD3~mbpgo4sX2L2Jwe~MCfUi^p?cyJLMud(%hX20`9T^ma!6bk;)oAq|+XH zvMBQn`1zmU)cQZ)UUL0b0PEGzDP$IMRZkGww4#U82=q*&73J80R5lvT;v*$c(KfKh z$CNXSd-?voLVF%{8_d9NloH8FL-K}tfmSiqgqjgUW%s@VYmzV&xgpj^@1R}`wZPY% z9o~fm`L2Uk5QN8*0OZXf0fKp**8Q?2jwMC#B9d7wJT-CX>}3cKbbnCJDuf;wL#tsE zuz<5rfOC=#ZzcMb-$M_jM}}1$#ubrz=xdcLrf`^vaVi`#! zibxh_wN}8Xeg7=laOLn?5~vT+TZ!LWMFt&)iCs znlcw98%>KImQE{Wc;k!g1p})7;jTSbflL)ZmUykmwPFlbdzIFVe(YB3!IQgWkzxlC zbq3WPg?v$c5n4a%raKrd#SEh0I)bRoqC(fj;(FYVE8H2IH~6#Ad4p>0t-ga?w<<}6 z*`wNyBI_mPRg7Pos0+g2R?)R01*J4v0A-n~_bc~|u0e!8!XIa!&%E*UXxG{mhpzgn7=IHCL;v;3Wy=~UaAQsUTJ z2D6<$Q^52!Ck14o_?+BPe-EK!!A$#*cUOHZnWTM zkP%ZbGkr)PG5M>iP~U~1pCdjZB^LQ`sofs6VlvTurp;UPGl^Boq!{y5TUY~%;j_F{ z=-{M!O+oi!cTQ;*_-K&rf5$ALEL)X}ahjNZ+bGYGG~n)?D6U%p#u(V7Zc6dyt=}7T zc$IQ&g1@JlA^56$VL3b;w^+MCGRt(QvnxG06i#m6tS^~w{rln!26+-)fWzDl6d$}6 z=c*^GKaF&D7GRl~(0X#04|l=YLWZ#LI?0roXA#72-NptKF2f(Qf;>Km`aa zIr`T;HNc?tl|qfLn?;c{8}gX7>@!*l@ujI5U(>^`+2Z5r$l32+T~*CNR9yYC|7PSO zY97tvw}W%M?~ZQeKn$ooLF0Zewua`!I~vsVVETGpns*db6B`)y@=FC`9b$19bgxd; zr=#J9c*iOJQDPd&`Z$LSRn`JkL?bXPtUom253g2y){Qq{+-yN`h+Y`Q^HUhAaMsZz zMaF4w{Hc5%^Zq$m`Ya@LcE3SRE4ATiI*ACOg?lNUH~0A_AVYj|`Lkwr?zty7-P_3A zAvS+qi9e-D_9xw(na4|dl6N>?p%5mqP@0=YqS$utoQe17S5oZhhoBJ6Y9u>J8}1 z4*^2#Q5*t%3g!^slEJ`^2pa0%=ne{FP#7mj5IH*+D}Z2hRp6L$x?mcd)VA3H>#~&d z1%BU!1b6V)JIFv*wh-Z<7zVY%(~mEqW267@13tGff(cS9&-u($xPq8oZPyY=0R7k8 zj%!DwBMP{*^t=kj)X;Dc|BK1OG@vx12kk(S_>Oo}Je-1GQ>=^9&|}R|BbHRM)$Td| z?sSB(saI-|g#-htL{9x=>#`r~au1dCoPxL#b2&@vL}<5Wwz;t2Vnj@uqAF{yWO3m_ z0(~4xY-UZOXeF>PEl>Ifafk#dSGBN^It$5f)b|Wx0a9?PSaDpcTmV!gYQc zvs7j(kk8^7B!m8dC`<-E+g9gcXShW@h9X5u;w^!3*f?reirH1XI*nv!lxA)j%|j;p zUTS1s!xL{AxuX$X&n|KXcUUwW8yIeJEF-0s#Jv`wO^8T7h3kQIZT6SXx*1YnJ_v&{;AM$X#c*5zNkFhC zJ10=~PP8x%HSg|>A0+B0-m@~H_^fye$;RGjGzo^BPa zl1_lt2Efl$e>-fP7t{L3C*v_ffejhaRH#CLsGu;Nn16Dek;yI-7C$f26{7YCaRCkI zaul6o@4Z|m(93jUTrk2t00!&_c}ZV2L#$XuCGEZ3%$Q!9n7kQ|l**}C97VCBaI=n3 zsh*}QQ4zhKX{}Whib^pBXM0)(#?wJ-LN6bDzK+81kBg->22$;!ADtcP z^=N(3N~iuaVRrDiY#596qxS)G`@aiv|0m+x>l#sktmw<`B1%9QxE*ijgk`iI`^k6> z7}x9wN_=A&KKd#8CBu>+A(~C&5~v7-f@5+G!_IbojF_TY5tQ-{aXHhz`u^l@Ue&$? zoVs-^@7757=3(@NR3m@~DU{NLv|r_Gm31n%Ck$X&@X+QEeSxbFY1pNV8%Bz3?k~}$ zWnxL=p=`b-641FJ$NtAC&6ps0gKRduei1~>%Z(@)gYZ=x;kt2jfBuivhpD`lG z0W*BmfQE^J$eh|H=OZ#0&1YgPDP7zJqIT(ZdAduOAJl4~<5h`SS;4kBjz#E5u0+7N zHJAnZ*bzcuuVou!x)iR7MeaGy*fj{7!mo0imZ*nLcYK6^Ov0FsF#|FCov zLg3S4vhk8&2c~jjfnwqgiRFkOEx`V43|qOaq_Jx`i0~NH@BwHbuDD3xu&8m4@InJ7 zL%ZS4Kq32GD>B^1GDT%RLXJvvWZ}%u7t-(q@&q2sp<>ve>}Qke^6;Y`n(_5!Jx2&4 z&u$7n%7OV&f}YGLp4{J>d~3v(DAASg+y2vof^jw7^tXst93M% zBK24j{HnK3na#@pscx>coS8?One&dM{MpFTFMty1xI(>M<9Fv7;jQt)o+cip&peOx zj^Wsj2*Pwmr+5}c&430$PoPwupz)5(zq8dQw0AjdYG7DH*jg5GXw-)Me1Hi>%AlZ8 zJMcBZG}f(p+K=1-9!2bW4Ka*)RU-RUBww9S>WnVBxX4JYlFm4CVT zb|vkDXUVMs15=UiN}9%!Xy{-i(Fx_6Fa!J;j+H?_QH`U%x^of!I2W5nY2)oA&|vwZtV|_9VJ##f$FSHZsh=rg7#^o_pIKaFYT#-Ru&7S3c3I^UD5)Olht7 zv*y1pV4bLDvYIBiHR&QrmCd^D<4MH_U`DB<6ycr#PY=0x2P&*Vi$iiF;^7@LUfS$Q zYJui@-IS+dmcV#_Dv~+9N!+Fcr#NKNVlpM!IZ9umXi9GhzEqon0%vM8PYxdmp=z&m zl`i45|EtyZ;9|sQhL6?H=!_+eYP%z{F4Ew3*2=mtELrNiB<@wrptvP#4$PwFYFJOt zU-5sVRdIiaWP5%Mj@5c$4sS6mFw}_&o6&jF%&L0?hw8asA)B$le##~MB%oK*j3*`d z17q8}j~<2SU>*X^x39#P>gO};RN>MDgvRT@&DV=@)6wE3CZ8BXNY5-4FhX4k zA&*Uhr$O~xP>y*7bP4E`ih%^W?EwYu&ZV8w(^|!YWpCGEW#K<9RARl`uTlA0%?n1{iS&te{InhC!ZqnzvFQ4j=&vEFq9ihTEw`tjH zQ@#~l>W4Vmd)p!*eoGOXp_Ra%0_{}NkT{}Zwk--Bw@``w=ik6j!M=rw+3x5)mmi23 zaODC2SZLcN5l@7&O_>HMr-dZOXHH?SwM_?wL;~V=i6Tq1FWjj_JJ!Nu-?+FZ4!TMV zo5j6O3CWBQe?RTuQ>VVh&)gvCfNY>x%3frIqVpy!s*9S=F{jM1R+mM9ps&9~MA)Sg zYlL)^vJ3(CTghKmK^FuWwRNXTCYG#K0L9JfHIMOxq}3oz8*SFN*nCI8GvfkdLwXQg z_@3A(;`FS!ir887Pt)})@2BZ|mfv$qQ_AK6%5nKzPhg(eY_|m`r>eE}tR$3rYpH;} zB?`peB|H76J#`fbOiQIWZ?t=F&}@FTp0}EAnGdgh`Qb91l7X_M!&j}OD|b-_8e8-J zAteD)@JJ>5ZzzKsLz?{vC@*z2{>p&bDlAk=kCP%3`=*n*D8*6yP|Sk24B#+FiDfiZ zaLAqlr?=uiDlmvaDqVGFH3xP*=0XBv7ca_Bj;hG5Fdq{?n9#RLwaI^Cb)4_D?fbhE zWWM>mD?7gr2g93(h_t;MPM?t9J+hp-s4nAN8c98O~$`Ns|=VbZ%Fo z$YB^;5{-HCe%J#F-!GRtC0iiDY-uEv9#+H@W)r7m^Ew~riV^{=C&>w*?E&iADMuZH zFWnrM3F7P*?Dbl+9oQHecSqdJAd@4-;8SA_eR8-{2^WNjf$q#aBMqd z-%)~;NC((~7$7>U+77mtDef69{?tU++)Bbsz?@uRmm@4Hfade>kCmJYX-EYz3X&Ar zSLv)s6mvWfZCV$`EXi~5<~f)M7{I~~oYYC$To>b@noB+L<^mku)&uj78FH@FmF966 zh77c!NU><9=dXggS+h)aT2>D<3TWXn zd9cnAS;;ctGn+l`C&=IvoBf}o=xmk9(r|5hNn}-fK6_x%AV1HEc+uQH-tcaO{{4J1 zLVC5>?*ld~ANP1O7^#8km-mxnAT;U@V~ofQKsgn`^sJyX;@{2VPG%sgR{}qvw4Aas z+*4G|{w%-`nH~c9fQ0T%?5}0LBCkk;P%H5zH<<0!fKq650kJzrwNb+j#ESUSG%rq+UIbGQ>bFzk>bzN*zFOk+ z`ZO>h(Yd>DF`4Ra>sFBd1)4S29DpehGBa1y&0GxjSzNsHkODr@lwOBTQYK; znJNA4TJZLoLxPuCa|v83({H0;kZTR{laL)7JWIhPXA?P#%`PbLR=}G@D6pcS?f{IA zAUP06UZZExn7~iftMl;v&dsf(GU=*WY1syur#cIebc8&(iClS-$jtW3$y)B+7S_m< zWD0gFFkntgFpGlbb~d1D&ov{|A6z74e{sq;LhHDr-E?a8GVk-RPK47{TxDJCXx#-& zJ~$Pg8K0!OFPkOIr^$LN?g>2KNOh!oKOx!mts%Gm45PbB*va07Vt2JG8?7mhslpY9Ki!m zhwhfy?lw}*$hY+}yUd1M+RG;#Z`q6

    $If@R%~T1rW)EOV^Gx{o3kRz2|U4pbc^v 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 { appState.navigationPanelJustClosed = value }, + + aiAssistantPanel: { open: false, editor: null, content: '' } as AiAssistantPanelType, + setAiAssistantPanel: (value: AiAssistantPanelType) => { + appState.aiAssistantPanel = { ...appState.aiAssistantPanel, ...value } + }, }) export const getAppStateSnapshot = () => snapshot(appState) diff --git a/packages/ai-commands/src/sql/chat.ts b/packages/ai-commands/src/sql/chat.ts index 3b8deb258f0..4b8457cd74f 100644 --- a/packages/ai-commands/src/sql/chat.ts +++ b/packages/ai-commands/src/sql/chat.ts @@ -13,26 +13,55 @@ export async function chatSql( openai: OpenAI, messages: Message[], existingSql?: string, - entityDefinitions?: string[] + entityDefinitions?: string[], + context?: 'rls-policies' | 'functions' ): Promise> { const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ { role: 'system', content: stripIndent` - The generated SQL (must be valid SQL). - - For primary keys, always use "id bigint primary key generated always as identity" (not serial) - - Prefer creating foreign key references in the create statement - - Prefer 'text' over 'varchar' - - Prefer 'timestamp with time zone' over 'date' - - Use vector(384) data type for any embedding/vector related query + The generated SQL (must be valid SQL), and must adhere to the following: - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - Always use semicolons - Output as markdown - Always include code snippets if available + - Use vector(384) data type for any embedding/vector related query + + When generating tables, do the following: + - For primary keys, always use "id bigint primary key generated always as identity" (not serial) + - Prefer creating foreign key references in the create statement + - Prefer 'text' over 'varchar' + - Prefer 'timestamp with time zone' over 'date' + + Feel free to suggest corrections for suspected typos. `, }, ] + if (context) { + // [Joshen] Thinking how we can prompt-engineer to give even better results, what I have below + // is definitely not optimal at all, but just to get an idea started + const generateInstructionsBasedOnContext = () => { + switch (context) { + case 'rls-policies': + return stripIndent` + You're a Supabase Postgres expert in writing row level security policies. Your purpose is to + generate a policy with the constraints given by the user. You will be provided a schema + on which the policy should be applied.` + case 'functions': + return stripIndent` + You're a Supabase Postgres expert in writing database functions. Your purpose is to generate a + database function with the constraints given by the user. The output may also include a database trigger + if the function returns a type of trigger. When generating functions, do the following: + - If the function returns a trigger type, ensure that it uses security definer, otherwise default to security invoker. Include this in the create functions SQL statement. + - Ensure to set the search_path configuration parameter as '', include this in the create functions SQL statement. + Please make sure that all queries are valid Postgres SQL queries. + ` + } + } + initMessages.push({ role: 'user', content: generateInstructionsBasedOnContext() }) + } + if (entityDefinitions) { const definitions = codeBlock`${entityDefinitions.join('\n\n')}` initMessages.push({ @@ -48,6 +77,7 @@ export async function chatSql( content: codeBlock` Here is the existing SQL I wrote for reference: ${sqlBlock} + Any SQL output should follow the casing of the existing SQL that I've written. `.trim(), }) } diff --git a/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx b/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx index 71885449bd0..4e384513e0b 100644 --- a/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx +++ b/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx @@ -68,7 +68,12 @@ const AssistantChatForm = React.forwardRef( } return ( - + {icon && (

    {icon} diff --git a/packages/ui-patterns/admonition.tsx b/packages/ui-patterns/admonition.tsx index 0754ec46867..eb4943d35b2 100644 --- a/packages/ui-patterns/admonition.tsx +++ b/packages/ui-patterns/admonition.tsx @@ -110,9 +110,9 @@ export const Admonition = forwardRef< > {(showIcon && typeMapped === 'warning') || typeMapped === 'destructive' ? ( - ) : ( + ) : showIcon ? ( - )} + ) : null} {label || title ? ( <> Date: Wed, 23 Oct 2024 13:50:33 +0200 Subject: [PATCH 29/80] add loki log drain form (#29848) * add loki log drain form * update form to use headers * add cursed icons * regen api types * add ff * update api client & fix icons * update api types --- .../LogDrainDestinationSheetForm.tsx | 181 +++++++++++------- .../LogDrains/LogDrains.constants.tsx | 18 +- .../interfaces/LogDrains/LogDrains.tsx | 28 +-- packages/api-types/types/api.d.ts | 113 ++--------- packages/icons/__registry__/index.tsx | 20 ++ packages/icons/src/icons/datadog.ts | 23 +++ packages/icons/src/icons/grafana.ts | 23 +++ packages/icons/src/icons/index.ts | 41 ++-- packages/icons/src/raw-icons/datadog.svg | 1 + packages/icons/src/raw-icons/grafana.svg | 12 ++ 10 files changed, 250 insertions(+), 210 deletions(-) create mode 100644 packages/icons/src/icons/datadog.ts create mode 100644 packages/icons/src/icons/grafana.ts create mode 100644 packages/icons/src/raw-icons/datadog.svg create mode 100644 packages/icons/src/raw-icons/grafana.svg diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx index c0c26f9a51d..a2d1e8a53f9 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx @@ -38,6 +38,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { DATADOG_REGIONS, LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants' import { urlRegex } from '../Auth/Auth.constants' +import { useFlag } from 'hooks/ui/useFlag' const FORM_ID = 'log-drain-destination-form' @@ -63,6 +64,11 @@ const formUnion = z.discriminatedUnion('type', [ z.object({ type: z.literal('bigquery'), }), + z.object({ + type: z.literal('loki'), + url: z.string().min(1, { message: 'Loki URL is required' }), + headers: z.record(z.string(), z.string()), + }), ]) const formSchema = z @@ -128,6 +134,7 @@ export function LogDrainDestinationSheetForm({ onSubmit: (values: z.infer) => void mode: 'create' | 'update' }) { + const lokiLogDrainsEnabled = useFlag('lokilogdrains') const CREATE_DEFAULT_HEADERS = { 'Content-Type': 'application/json', } @@ -197,6 +204,16 @@ export function LogDrainDestinationSheetForm({ } }, [mode, open, form]) + function getHeadersSectionDescription() { + if (type === 'webhook') { + return 'Set custom headers when draining logs to the Endpoint URL' + } + if (type === 'loki') { + return 'Set custom headers when draining logs to the Loki HTTP(S) endpoint' + } + return '' + } + return ( t.value === type)?.name} - {LOG_DRAIN_TYPES.map((type) => ( - - {type.name} - - ))} + {LOG_DRAIN_TYPES.map((type) => + type.value === 'loki' && !lokiLogDrainsEnabled ? null : ( + + {type.name} + + ) + )} @@ -309,7 +328,6 @@ export function LogDrainDestinationSheetForm({ - )} /> @@ -332,36 +350,6 @@ export function LogDrainDestinationSheetForm({ )} /> - -
    -
    - Custom Headers -

    - Set custom headers when draining logs to the Endpoint URL -

    -
    -
    - {hasHeaders && - Object.keys(headers || {})?.map((headerKey) => ( -
    -
    {headerKey}
    -
    - {headers?.[headerKey]} -
    - -
    - ))} -
    -
    )} {type === 'datadog' && ( @@ -429,45 +417,90 @@ export function LogDrainDestinationSheetForm({ />
    )} + {type === 'loki' && ( +
    + +
    + )} + {/* This form needs to be outside the */} - {type === 'webhook' && ( -
    { - e.preventDefault() - e.stopPropagation() - addHeader() - }} - className="flex border-t py-4 gap-4 items-center px-content" - > - - setNewCustomHeader({ ...newCustomHeader, name: e.target.value })} - /> - - setNewCustomHeader({ ...newCustomHeader, value: e.target.value })} - /> + {(type === 'webhook' || type === 'loki') && ( + <> +
    +
    +

    Custom Headers

    +

    + {getHeadersSectionDescription()} +

    +
    +
    + {hasHeaders && + Object.keys(headers || {})?.map((headerKey) => ( +
    +
    {headerKey}
    +
    + {headers?.[headerKey]} +
    + +
    + ))} +
    +
    + { + e.preventDefault() + e.stopPropagation() + addHeader() + }} + className="flex border-t py-4 gap-4 items-center px-content" + > + + setNewCustomHeader({ ...newCustomHeader, name: e.target.value })} + /> + + + setNewCustomHeader({ ...newCustomHeader, value: e.target.value }) + } + /> - - + + + )} diff --git a/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx b/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx index 8a00b1e3e30..e73608d482d 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx @@ -1,6 +1,11 @@ -import { BracesIcon, DogIcon } from 'lucide-react' +import { BracesIcon } from 'lucide-react' +import { Datadog, Grafana } from 'icons' -const iconProps = { size: 24, strokeWidth: 1.5, className: 'text-foreground-light' } +const iconProps = { + height: 24, + width: 24, + className: 'text-foreground-light', +} export const LOG_DRAIN_TYPES = [ { @@ -13,7 +18,14 @@ export const LOG_DRAIN_TYPES = [ value: 'datadog', name: 'Datadog', description: 'Datadog is a monitoring service for cloud-scale applications', - icon: , + icon: , + }, + { + value: 'loki', + name: 'Loki', + description: + 'Loki is an open-source log aggregation system designed to store and query logs from multiple sources', + icon: , }, ] as const diff --git a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx index ce41a14e033..894832547f7 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx @@ -27,6 +27,7 @@ import { import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants' +import { useFlag } from 'hooks/ui/useFlag' export function LogDrains({ onNewDrainClick, @@ -39,6 +40,7 @@ export function LogDrains({ const { isLoading: orgPlanLoading, plan } = useCurrentOrgPlan() const logDrainsEnabled = !orgPlanLoading && (plan?.id === 'team' || plan?.id === 'enterprise') + const lokiLogDrainsEnabled = useFlag('lokilogdrains') const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [selectedLogDrain, setSelectedLogDrain] = useState(null) @@ -90,18 +92,20 @@ export function LogDrains({ if (!isLoading && logDrains?.length === 0) { return ( -
    - {LOG_DRAIN_TYPES.map((src) => ( - { - onNewDrainClick(src.value) - }} - /> - ))} +
    + {LOG_DRAIN_TYPES.map((src) => + src.value === 'loki' && !lokiLogDrainsEnabled ? null : ( + { + onNewDrainClick(src.value) + }} + /> + ) + )}
    ) } diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 3a85142fd88..b9504458a6e 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -39,10 +39,6 @@ export interface paths { /** Gets details of the organization linked to the provided Fly organization id */ get: operations['FlyOrganizationsController_getOrganization'] } - '/partners/flyio/organizations/{organization_id}/billing': { - /** Gets the organizations current unbilled charges */ - get: operations['FlyBillingController_getResourceBilling'] - } '/partners/flyio/organizations/{organization_id}/extensions': { /** Gets all databases that belong to the given Fly organization id */ get: operations['FlyOrganizationsController_getOrgExtensions'] @@ -1189,10 +1185,6 @@ export interface paths { /** Processes Orb events */ post: operations['OrbWebhooksController_processEvent'] } - '/system/organizations/{slug}/billing/partner/usage-and-costs': { - /** Gets the partner usage and costs */ - get: operations['PartnerBillingSystemController_getPartnerUsageAndCosts'] - } '/system/organizations/{slug}/billing/subscription': { /** Gets the current subscription */ get: operations['OrgSubscriptionSystemController_getSubscription'] @@ -1241,7 +1233,7 @@ export interface paths { * Create a function * @description Creates a function and adds it to the specified project. */ - post: operations['SystemFunctionsController_createFunction'] + post: operations['v1-create-a-function'] /** Deletes all Edge Functions from a project */ delete: operations['SystemFunctionsController_systemDeleteAllFunctions'] } @@ -2137,7 +2129,7 @@ export interface paths { * Create a function * @description Creates a function and adds it to the specified project. */ - post: operations['FunctionsController_createFunction'] + post: operations['v1-create-a-function'] } '/v1/projects/{ref}/functions/{function_slug}': { /** @@ -2763,7 +2755,7 @@ export interface components { description?: string name: string /** @enum {string} */ - type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic' + type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic' | 'loki' } CreateBranchBody: { branch_name: string @@ -4023,7 +4015,7 @@ export interface components { name: string token: string /** @enum {string} */ - type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic' + type: 'postgres' | 'bigquery' | 'webhook' | 'datadog' | 'elastic' | 'loki' user_id: number } LFEndpoint: { @@ -5095,47 +5087,6 @@ export interface components { ResizeBody: { volume_size_gb: number } - ResourceBillingItem: { - /** - * @description Costs of the item in cents - * @example 100 - */ - costs: number - /** - * @description In case of a usage item, the free usage included in the customers Plan - * @example 100 - */ - freeUnitsInPlan?: number - /** - * @description Non-Unique identifier of the item - * @example usage_egress - */ - itemIdentifier: string - /** - * @description Descriptive name of the billing item - * @example Pro Plan - */ - itemName: string - /** @enum {string} */ - type: 'usage' | 'plan' | 'addon' | 'proration' | 'compute_credits' - /** - * @description In case of a usage item, the billable usage amount, free usage has been deducted - * @example 100 - */ - usageBillable?: number - /** - * @description In case of a usage item, the total usage - * @example 100 - */ - usageTotal?: number - } - ResourceBillingResponse: { - /** @description Whether the user is exceeding the included quotas in the Plan - only relevant for users on usage-capped Plans. */ - exceedsPlanLimits: boolean - items: components['schemas']['ResourceBillingItem'][] - /** @description Whether the user is can have over-usage, which will be billed - this will be false on usage-capped Plans. */ - overusageAllowed: boolean - } ResourceProvisioningConfigResponse: { /** * @description Pooler connection string @@ -7042,21 +6993,6 @@ export interface operations { } } } - /** Gets the organizations current unbilled charges */ - FlyBillingController_getResourceBilling: { - parameters: { - path: { - organization_id: string - } - } - responses: { - 200: { - content: { - 'application/json': components['schemas']['ResourceBillingResponse'] - } - } - } - } /** Gets all databases that belong to the given Fly organization id */ FlyOrganizationsController_getOrgExtensions: { parameters: { @@ -10074,19 +10010,11 @@ export interface operations { } } } - /** - * Create a function - * @description Creates a function and adds it to the specified project. - */ + /** Creates project pg.function */ FunctionsController_createFunction: { parameters: { - query?: { - slug?: string - name?: string - verify_jwt?: boolean - import_map?: boolean - entrypoint_path?: string - import_map_path?: string + header: { + 'x-connection-encrypted': string } path: { /** @description Project ref */ @@ -10095,20 +10023,19 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['V1CreateFunctionBody'] - 'application/vnd.denoland.eszip': components['schemas']['V1CreateFunctionBody'] + 'application/json': components['schemas']['CreateFunctionBody'] } } responses: { 201: { content: { - 'application/json': components['schemas']['FunctionResponse'] + 'application/json': components['schemas']['PostgresFunction'] } } 403: { content: never } - /** @description Failed to create project's function */ + /** @description Failed to create pg.function */ 500: { content: never } @@ -14497,24 +14424,6 @@ export interface operations { } } } - /** Gets the partner usage and costs */ - PartnerBillingSystemController_getPartnerUsageAndCosts: { - parameters: { - path: { - /** @description Organization slug */ - slug: string - } - } - responses: { - 200: { - content: never - } - /** @description Failed to retrieve subscription */ - 500: { - content: never - } - } - } /** Gets the current subscription */ OrgSubscriptionSystemController_getSubscription: { parameters: { @@ -14751,7 +14660,7 @@ export interface operations { * Create a function * @description Creates a function and adds it to the specified project. */ - SystemFunctionsController_createFunction: { + 'v1-create-a-function': { parameters: { query?: { slug?: string diff --git a/packages/icons/__registry__/index.tsx b/packages/icons/__registry__/index.tsx index 7dc292a1252..3d22f40f027 100644 --- a/packages/icons/__registry__/index.tsx +++ b/packages/icons/__registry__/index.tsx @@ -54,6 +54,16 @@ export const Index: Record = [ svg: "\n \n \n \n", jsx: "import { Database } from \"icons\"\n \n " }, +{ + name: "datadog", + componentName: "Datadog", + deprecated: false, + raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name Datadog\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIwIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgaGVpZ2h0PSIyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xOS41NyAxNy4wNGwtMS45OTctMS4zMTYtMS42NjUgMi43ODItMS45MzctLjU2Ny0xLjcwNiAyLjYwNC4wODcuODIgOS4yNzQtMS43MS0uNTM4LTUuNzk0em0tOC42NDktMi40OThsMS40ODgtLjIwNGMuMjQxLjEwOC40MDkuMTUuNjk3LjIyMy40NS4xMTcuOTcuMjMgMS43NDEtLjE2LjE4LS4wODguNTUzLS40My43MDQtLjYyNWw2LjA5Ni0xLjEwNi42MjIgNy41MjctMTAuNDQ0IDEuODgyem0xMS4zMjUtMi43MTJsLS42MDIuMTE1TDIwLjQ4OCAwIC43ODkgMi4yODVsMi40MjcgMTkuNjkzIDIuMzA2LS4zMzRjLS4xODQtLjI2My0uNDcxLS41ODEtLjk2LS45ODktLjY4LS41NjQtLjQ0LTEuNTIyLS4wMzktMi4xMjcuNTMtMS4wMjIgMy4yNi0yLjMyMiAzLjEwNi0zLjk1Ni0uMDU2LS41OTQtLjE1LTEuMzY4LS43MDItMS44OTgtLjAyLjIyLjAxNy40MzIuMDE3LjQzMnMtLjIyNy0uMjg5LS4zNC0uNjgzYy0uMTEyLS4xNS0uMi0uMTk5LS4zMTktLjQtLjA4NS4yMzMtLjA3My41MDMtLjA3My41MDNzLS4xODYtLjQzNy0uMjE2LS44MDdjLS4xMS4xNjYtLjEzNy40OC0uMTM3LjQ4cy0uMjQxLS42OS0uMTg2LTEuMDYyYy0uMTEtLjMyMy0uNDM2LS45NjUtLjM0My0yLjQyNC42LjQyMSAxLjkyNC4zMjEgMi40NC0uNDM5LjE3MS0uMjUxLjI4OC0uOTM5LS4wODYtMi4yOTMtLjI0LS44NjgtLjgzNS0yLjE2LTEuMDY2LTIuNjUxbC0uMDI4LjAyYy4xMjIuMzk1LjM3NCAxLjIyMy40NyAxLjYyNS4yOTMgMS4yMTguMzcyIDEuNjQyLjIzNCAyLjIwNC0uMTE2LjQ4OC0uMzk3LjgwOC0xLjEwNyAxLjE2NS0uNzEuMzU4LTEuNjUzLS41MTQtMS43MTMtLjU2Mi0uNjktLjU1LTEuMjI0LTEuNDQ3LTEuMjg0LTEuODgzLS4wNjItLjQ3Ny4yNzUtLjc2My40NDUtMS4xNTMtLjI0My4wNy0uNTE0LjE5Mi0uNTE0LjE5MnMuMzIzLS4zMzQuNzIyLS42MjRjLjE2NS0uMTA5LjI2Mi0uMTc4LjQzNi0uMzIzYTkuNzYyIDkuNzYyIDAgMCAwLS40NTYuMDAzcy40Mi0uMjI3Ljg1NS0uMzkyYy0uMzE4LS4wMTQtLjYyMy0uMDAzLS42MjMtLjAwM3MuOTM3LS40MTkgMS42NzgtLjcyN2MuNTA5LS4yMDggMS4wMDYtLjE0NyAxLjI4Ni4yNTcuMzY3LjUzLjc1Mi44MTcgMS41NjkuOTk2LjUwMS0uMjIzLjY1My0uMzM3IDEuMjg0LS41MDkuNTU0LS42MS45OS0uNjg4Ljk5LS42ODhzLS4yMTYuMTk4LS4yNzQuNTFjLjMxNC0uMjQ5LjY2LS40NTUuNjYtLjQ1NXMtLjEzNC4xNjQtLjI1OS40MjZsLjAzLjA0M2MuMzY2LS4yMi43OTctLjM5NC43OTctLjM5NHMtLjEyMy4xNTYtLjI2OC4zNThjLjI3Ny0uMDAyLjgzOC4wMTIgMS4wNTYuMDM3IDEuMjg1LjAyOCAxLjU1Mi0xLjM3NCAyLjA0NS0xLjU1LjYxOC0uMjIuODk0LS4zNTMgMS45NDcuNjguOTAzLjg4OCAxLjYwOSAyLjQ3NyAxLjI1OSAyLjgzMy0uMjk0LjI5NS0uODc0LS4xMTUtMS41MTYtLjkxNmEzLjQ2NiAzLjQ2NiAwIDAgMS0uNzE2LTEuNTYyIDEuNTMzIDEuNTMzIDAgMCAwLS40OTctLjg1cy4yMy41MS4yMy45NmMwIC4yNDYuMDMgMS4xNjUuNDI0IDEuNjgtLjAzOS4wNzYtLjA1Ny4zNzQtLjEuNDMtLjQ1OC0uNTU0LTEuNDQzLS45NS0xLjYwNC0xLjA2Ny41NDQuNDQ1IDEuNzkzIDEuNDY4IDIuMjczIDIuNDQ5LjQ1My45MjcuMTg2IDEuNzc3LjQxNiAxLjk5Ny4wNjUuMDYzLjk3NiAxLjE5NyAxLjE1IDEuNzY3LjMwNi45OTQuMDE5IDIuMDM4LS4zODEgMi42ODVsLTEuMTE3LjE3NGMtLjE2My0uMDQ1LS4yNzMtLjA2OC0uNDItLjE1My4wOC0uMTQzLjI0MS0uNS4yNDMtLjU3MmwtLjA2My0uMTExYy0uMzQ4LjQ5Mi0uOTMuOTctMS40MTQgMS4yNDUtLjYzMy4zNTktMS4zNjMuMzA0LTEuODM4LjE1Ni0xLjM0OC0uNDE1LTIuNjIzLTEuMzI3LTIuOTMtMS41NjYgMCAwLS4wMS4xOTEuMDQ4LjIzNC4zNC4zODMgMS4xMTkgMS4wNzcgMS44NzIgMS41NmwtMS42MDUuMTc3Ljc1OSA1LjkwOGMtLjMzNy4wNDgtLjM5LjA3MS0uNzU3LjEyNC0uMzI1LTEuMTQ3LS45NDYtMS44OTUtMS42MjQtMi4zMzItLjU5OS0uMzg0LTEuNDI0LS40Ny0yLjIxNC0uMzE0bC0uMDUuMDU5YTIuODUxIDIuODUxIDAgMCAxIDEuODYzLjQ0NGMuNjU0LjQxMyAxLjE4MSAxLjQ4MSAxLjM3NSAyLjEyNC4yNDguODIyLjQyIDEuNy0uMjQ4IDIuNjMyLS40NzYuNjYyLTEuODY0IDEuMDI4LTIuOTg2LjIzNy4zLjQ4MS43MDUuODc2IDEuMjUuOTUuODA5LjExIDEuNTc3LS4wMyAyLjEwNi0uNTc0LjQ1Mi0uNDY0LjY5LTEuNDM0LjYyOC0yLjQ1NmwuNzE0LS4xMDQuMjU4IDEuODM0IDExLjgyNy0xLjQyNHpNMTUuMDUgNi44NDhjLS4wMzQuMDc1LS4wODUuMTI1LS4wMDcuMzdsLjAwNC4wMTQuMDEzLjAzMi4wMzIuMDczYy4xNC4yODcuMjk1LjU1OC41NTIuNjk2LjA2Ny0uMDExLjEzNi0uMDE5LjIwNy0uMDIzLjI0Mi0uMDEuMzk1LjAyOC40OTIuMDguMDA5LS4wNDguMDEtLjExOS4wMDUtLjIyMi0uMDE4LS4zNjQuMDcyLS45ODItLjYyNi0xLjMwOC0uMjY0LS4xMjItLjYzNC0uMDg0LS43NTcuMDY4YS4zMDIuMzAyIDAgMCAxIC4wNTguMDEzYy4xODYuMDY2LjA2LjEzLjAyNy4yMDdtMS45NTggMy4zOTJjLS4wOTItLjA1LS41Mi0uMDMtLjgyMS4wMDUtLjU3NC4wNjgtMS4xOTMuMjY3LTEuMzI4LjM3Mi0uMjQ3LjE5MS0uMTM1LjUyMy4wNDcuNjYuNTExLjM4Mi45Ni42MzggMS40MzIuNTc1LjI5LS4wMzguNTQ2LS40OTcuNzI4LS45MTQuMTI0LS4yODguMTI0LS41OTgtLjA1OC0uNjk4bS01LjA3Ny0yLjk0MmMuMTYyLS4xNTQtLjgwNS0uMzU1LTEuNTU2LjE1Ni0uNTU0LjM3OC0uNTcxIDEuMTg3LS4wNDEgMS42NDYuMDUzLjA0Ni4wOTYuMDc4LjEzNy4xMDRhNC43NyA0Ljc3IDAgMCAxIDEuMzk2LS40MTJjLjExMy0uMTI1LjI0My0uMzQ1LjIxLS43NDUtLjA0NC0uNTQyLS40NTUtLjQ1Ni0uMTQ2LS43NDkiPjwvcGF0aD48L3N2Zz4=)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst Datadog = createSupabaseIcon('Datadog', [\n [\n 'path',\n {\n d: 'M19.57 17.04l-1.997-1.316-1.665 2.782-1.937-.567-1.706 2.604.087.82 9.274-1.71-.538-5.794zm-8.649-2.498l1.488-.204c.241.108.409.15.697.223.45.117.97.23 1.741-.16.18-.088.553-.43.704-.625l6.096-1.106.622 7.527-10.444 1.882zm11.325-2.712l-.602.115L20.488 0 .789 2.285l2.427 19.693 2.306-.334c-.184-.263-.471-.581-.96-.989-.68-.564-.44-1.522-.039-2.127.53-1.022 3.26-2.322 3.106-3.956-.056-.594-.15-1.368-.702-1.898-.02.22.017.432.017.432s-.227-.289-.34-.683c-.112-.15-.2-.199-.319-.4-.085.233-.073.503-.073.503s-.186-.437-.216-.807c-.11.166-.137.48-.137.48s-.241-.69-.186-1.062c-.11-.323-.436-.965-.343-2.424.6.421 1.924.321 2.44-.439.171-.251.288-.939-.086-2.293-.24-.868-.835-2.16-1.066-2.651l-.028.02c.122.395.374 1.223.47 1.625.293 1.218.372 1.642.234 2.204-.116.488-.397.808-1.107 1.165-.71.358-1.653-.514-1.713-.562-.69-.55-1.224-1.447-1.284-1.883-.062-.477.275-.763.445-1.153-.243.07-.514.192-.514.192s.323-.334.722-.624c.165-.109.262-.178.436-.323a9.762 9.762 0 0 0-.456.003s.42-.227.855-.392c-.318-.014-.623-.003-.623-.003s.937-.419 1.678-.727c.509-.208 1.006-.147 1.286.257.367.53.752.817 1.569.996.501-.223.653-.337 1.284-.509.554-.61.99-.688.99-.688s-.216.198-.274.51c.314-.249.66-.455.66-.455s-.134.164-.259.426l.03.043c.366-.22.797-.394.797-.394s-.123.156-.268.358c.277-.002.838.012 1.056.037 1.285.028 1.552-1.374 2.045-1.55.618-.22.894-.353 1.947.68.903.888 1.609 2.477 1.259 2.833-.294.295-.874-.115-1.516-.916a3.466 3.466 0 0 1-.716-1.562 1.533 1.533 0 0 0-.497-.85s.23.51.23.96c0 .246.03 1.165.424 1.68-.039.076-.057.374-.1.43-.458-.554-1.443-.95-1.604-1.067.544.445 1.793 1.468 2.273 2.449.453.927.186 1.777.416 1.997.065.063.976 1.197 1.15 1.767.306.994.019 2.038-.381 2.685l-1.117.174c-.163-.045-.273-.068-.42-.153.08-.143.241-.5.243-.572l-.063-.111c-.348.492-.93.97-1.414 1.245-.633.359-1.363.304-1.838.156-1.348-.415-2.623-1.327-2.93-1.566 0 0-.01.191.048.234.34.383 1.119 1.077 1.872 1.56l-1.605.177.759 5.908c-.337.048-.39.071-.757.124-.325-1.147-.946-1.895-1.624-2.332-.599-.384-1.424-.47-2.214-.314l-.05.059a2.851 2.851 0 0 1 1.863.444c.654.413 1.181 1.481 1.375 2.124.248.822.42 1.7-.248 2.632-.476.662-1.864 1.028-2.986.237.3.481.705.876 1.25.95.809.11 1.577-.03 2.106-.574.452-.464.69-1.434.628-2.456l.714-.104.258 1.834 11.827-1.424zM15.05 6.848c-.034.075-.085.125-.007.37l.004.014.013.032.032.073c.14.287.295.558.552.696.067-.011.136-.019.207-.023.242-.01.395.028.492.08.009-.048.01-.119.005-.222-.018-.364.072-.982-.626-1.308-.264-.122-.634-.084-.757.068a.302.302 0 0 1 .058.013c.186.066.06.13.027.207m1.958 3.392c-.092-.05-.52-.03-.821.005-.574.068-1.193.267-1.328.372-.247.191-.135.523.047.66.511.382.96.638 1.432.575.29-.038.546-.497.728-.914.124-.288.124-.598-.058-.698m-5.077-2.942c.162-.154-.805-.355-1.556.156-.554.378-.571 1.187-.041 1.646.053.046.096.078.137.104a4.77 4.77 0 0 1 1.396-.412c.113-.125.243-.345.21-.745-.044-.542-.455-.456-.146-.749',\n key: '13qbml',\n },\n ],\n]);\n\nexport default Datadog;\n", + component: React.lazy(() => import('icons/src/icons/datadog')), + import: "import { Datadog } from 'icons'", + svg: "", + jsx: "import { Datadog } from \"icons\"\n \n " +}, { name: "edge-functions", componentName: "EdgeFunctions", @@ -64,6 +74,16 @@ export const Index: Record = [ svg: "\n \n \n \n \n", jsx: "import { EdgeFunctions } from \"icons\"\n \n " }, +{ + name: "grafana", + componentName: "Grafana", + deprecated: false, + raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name Grafana\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyAgICBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgICBzdHJva2Utd2lkdGg9IjAiCiAgICByb2xlPSJpbWciCiAgICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgICBoZWlnaHQ9IjI0IgogICAgd2lkdGg9IjI0IgogICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogID4KICAgIDxwYXRoIGQ9Ik0yMy4wMiAxMC41OWE4LjU3OCA4LjU3OCAwIDAgMC0uODYyLTMuMDM0IDguOTExIDguOTExIDAgMCAwLTEuNzg5LTIuNDQ1Yy4zMzctMS4zNDItLjQxMy0yLjUwNS0uNDEzLTIuNTA1LTEuMjkyLS4wOC0yLjExMy40LTIuNDE2LjYyLS4wNTItLjAyLS4xMDItLjA0NC0uMTU0LS4wNjQtLjIyLS4wODktLjQ0Ni0uMTcyLS42NzctLjI0Ny0uMjMxLS4wNzMtLjQ3LS4xNC0uNzExLS4xOTdhOS44NjcgOS44NjcgMCAwIDAtLjg3NS0uMTYxQzE0LjU1Ny43NTMgMTIuOTQgMCAxMi45NCAwYy0xLjgwNCAxLjE0NS0yLjE0NyAyLjc0NC0yLjE0NyAyLjc0NGwtLjAxOC4wOTNjLS4wOTguMDI5LS4yLjA1Ny0uMjk4LjA4OC0uMTM4LjA0Mi0uMjc1LjA5NC0uNDEzLjE0My0uMTM4LjA1NS0uMjc1LjEwNy0uNDEuMTY2YTguODY5IDguODY5IDAgMCAwLTEuNTU3Ljg3bC0uMDYzLS4wMjljLTIuNDk3LS45NTUtNC43MTYuMTk1LTQuNzE2LjE5NS0uMjAzIDIuNjU4Ljk5NiA0LjMzIDEuMjM1IDQuNjM2YTExLjYwOCAxMS42MDggMCAwIDAtLjYwNyAyLjYzNUMxLjYzNiAxMi42NzcuOTUzIDE1LjAxNC45NTMgMTUuMDE0YzEuOTI2IDIuMjE0IDQuMTcxIDIuMzUxIDQuMTcxIDIuMzUxLjAwMy0uMDAyLjAwNi0uMDAyLjAwNi0uMDA1LjI4NS41MDkuNjE1Ljk5NC45ODYgMS40NDYuMTU2LjE5LjMyLjM3MS40ODguNTQ4LS43MDQgMi4wMDkuMDk5IDMuNjguMDk5IDMuNjggMi4xNDQuMDggMy41NTMtLjkzNyAzLjg0OS0xLjE3M2E5Ljc4NCA5Ljc4NCAwIDAgMCAzLjE2NC41MDFoLjA4bC4wNTUtLjAwMy4xMDctLjAwMi4xMDMtLjAwNS4wMDMuMDAyYzEuMDEgMS40NCAyLjc4OCAxLjY0NiAyLjc4OCAxLjY0NiAxLjI2NC0xLjMzMiAxLjMzNy0yLjY1MyAxLjMzNy0yLjk0di0uMDU4YzAtLjAyLS4wMDMtLjAzOS0uMDAzLS4wNi4yNjUtLjE4Ny41Mi0uMzg3Ljc1OC0uNmE3Ljg3NSA3Ljg3NSAwIDAgMCAxLjQxNS0xLjdjMS40My4wODMgMi40MzctLjg4NSAyLjQzNy0uODg1LS4yMzYtMS40OS0xLjA4NS0yLjIxNi0xLjI2NC0yLjM1NGwtLjAxOC0uMDEzLS4wMTYtLjAxM2EuMjE3LjIxNyAwIDAgMS0uMDMxLS4wMmMuMDA4LS4wOTIuMDE2LS4xOC4wMi0uMjcuMDExLS4xNjIuMDE2LS4zMjMuMDE2LS40OHYtLjI1M2wtLjAwNS0uMDk4LS4wMDgtLjEzNWExLjg5MSAxLjg5MSAwIDAgMC0uMDEtLjEzYy0uMDAzLS4wNDItLjAwOC0uMDgzLS4wMTMtLjEyNWwtLjAxNi0uMTI0LS4wMTgtLjEyMmE2LjIxNSA2LjIxNSAwIDAgMC0yLjAzMi0zLjczIDYuMDE1IDYuMDE1IDAgMCAwLTMuMjIyLTEuNDYgNi4yOTIgNi4yOTIgMCAwIDAtLjg1LS4wNDhsLS4xMDcuMDAyaC0uMDYzbC0uMDQ0LjAwMy0uMTA0LjAwOGE0Ljc3NyA0Ljc3NyAwIDAgMC0zLjMzNSAxLjY5NWMtLjMzMi40LS41OTIuODQtLjc2OCAxLjI5N2E0LjU5NCA0LjU5NCAwIDAgMC0uMzEyIDEuODE3bC4wMDMuMDkxYy4wMDUuMDU1LjAwNy4xMS4wMTMuMTY0YTMuNjE1IDMuNjE1IDAgMCAwIC42OTggMS44MiAzLjUzIDMuNTMgMCAwIDAgMS44MjcgMS4yODJjLjMzLjA5OC42Ni4xNC45NzEuMTM3LjAzOSAwIC4wNzggMCAuMTE0LS4wMDJsLjA2My0uMDAzYy4wMiAwIC4wNDEtLjAwMy4wNjItLjAwMy4wMzQtLjAwMi4wNjUtLjAwNy4wOTktLjAxLjAwNyAwIC4wMTgtLjAwMy4wMjgtLjAwM2wuMDMxLS4wMDUuMDYtLjAwOGExLjE4IDEuMTggMCAwIDAgLjExMi0uMDJjLjAzNi0uMDA4LjA3Mi0uMDEzLjEwOS0uMDI0YTIuNjM0IDIuNjM0IDAgMCAwIC45MTQtLjQxNWMuMDI4LS4wMi4wNTYtLjA0MS4wODUtLjA2NWEuMjQ4LjI0OCAwIDAgMCAuMDM5LS4zNS4yNDQuMjQ0IDAgMCAwLS4zMDktLjA2bC0uMDc4LjA0MmMtLjA5LjA0NC0uMTg0LjA4My0uMjgzLjExNmEyLjQ3NiAyLjQ3NiAwIDAgMS0uNDc1LjA5NmMtLjAyOC4wMDMtLjA1NC4wMDYtLjA4My4wMDZsLS4wODMuMDAyYy0uMDI2IDAtLjA1NCAwLS4wOC0uMDAybC0uMTAyLS4wMDZoLS4wMTJsLS4wMjQuMDA2Yy0uMDE2LS4wMDMtLjAzMS0uMDAzLS4wNDQtLjAwNi0uMDMxLS4wMDItLjA2LS4wMDctLjA5MS0uMDFhMi41OSAyLjU5IDAgMCAxLS43MjQtLjIxMyAyLjU1NyAyLjU1NyAwIDAgMS0uNjY3LS40MzggMi41MiAyLjUyIDAgMCAxLS44MDUtMS40NzUgMi4zMDYgMi4zMDYgMCAwIDEtLjAyOS0uNDQ0bC4wMDYtLjEyMnYtLjAyM2wuMDAyLS4wMzFjLjAwMy0uMDIxLjAwMy0uMDQuMDA1LS4wNmEzLjE2MyAzLjE2MyAwIDAgMSAxLjM1Mi0yLjI5IDMuMTIgMy4xMiAwIDAgMSAuOTM3LS40MyAyLjk0NiAyLjk0NiAwIDAgMSAuNzc2LS4xMDFoLjA2bC4wNy4wMDIuMDQ1LjAwM2guMDI2bC4wNy4wMDVhNC4wNDEgNC4wNDEgMCAwIDEgMS42MzUuNDkgMy45NCAzLjk0IDAgMCAxIDEuNjAyIDEuNjYyIDMuNzcgMy43NyAwIDAgMSAuMzk3IDEuNDE0bC4wMDUuMDc2LjAwMy4wNzVjLjAwMi4wMjYuMDAyLjA1LjAwMi4wNzUgMCAuMDI0LjAwMy4wNTIgMCAuMDd2LjA2NWwtLjAwMi4wNzMtLjAwOC4xNzRhNi4xOTUgNi4xOTUgMCAwIDEtLjA4LjYzOSA1LjEgNS4xIDAgMCAxLS4yNjcuOTI3IDUuMzEgNS4zMSAwIDAgMS0uNjI0IDEuMTMgNS4wNTIgNS4wNTIgMCAwIDEtMy4yMzcgMi4wMTQgNC44MiA0LjgyIDAgMCAxLS42NDkuMDY2bC0uMDM5LjAwM2gtLjI4N2E2LjYwNyA2LjYwNyAwIDAgMS0xLjcxNi0uMjY1IDYuNzc2IDYuNzc2IDAgMCAxLTMuNC0yLjI3NCA2Ljc1IDYuNzUgMCAwIDEtLjc0Ni0xLjE1IDYuNjE2IDYuNjE2IDAgMCAxLS43MTQtMi41OTZsLS4wMDUtLjA4My0uMDAyLS4wMnYtLjA1NmwtLjAwMy0uMDczdi0uMDk2bC0uMDAzLS4xMDR2LS4wN2wuMDAzLS4xNjNjLjAwOC0uMjIuMDI2LS40NS4wNTQtLjY3OGE4LjcwNyA4LjcwNyAwIDAgMSAuMjgtMS4zNTVjLjEyOC0uNDQ0LjI4Ni0uODcyLjQ3My0xLjI3N2E3LjA0IDcuMDQgMCAwIDEgMS40NTYtMi4xIDUuOTI1IDUuOTI1IDAgMCAxIC45NTMtLjc2M2MuMTY5LS4xMTEuMzQzLS4yMTMuNTI0LS4zMDYuMDg5LS4wNS4xODItLjA5MS4yNzMtLjEzNS4wNDctLjAyLjA5My0uMDQyLjEzOC0uMDYyYTcuMTc3IDcuMTc3IDAgMCAxIC43MTQtLjI2N2wuMTQ1LS4wNDVjLjA0OS0uMDE1LjA5OC0uMDI2LjE0OC0uMDQxLjA5OC0uMDI5LjE5Ny0uMDUyLjI5Ni0uMDc2LjA0OS0uMDEzLjEtLjAyLjE1LS4wMzNsLjE1LS4wMzIuMTUxLS4wMjguMDc2LS4wMTMuMDc1LS4wMS4xNTMtLjAyNGMuMDU3LS4wMS4xMTQtLjAxMy4xNzEtLjAyM2wuMTY5LS4wMjFjLjAzNi0uMDAzLjA3My0uMDA4LjEwNi0uMDFsLjA3My0uMDA4LjAzNi0uMDAzLjA0Mi0uMDAyYy4wNTctLjAwMy4xMTQtLjAwOC4xNzEtLjAxbC4wODYtLjAwNmguMDIzbC4wMzctLjAwMy4xNDUtLjAwN2E3Ljk5OSA3Ljk5OSAwIDAgMSAxLjcwOC4xMjUgNy45MTcgNy45MTcgMCAwIDEgMi4wNDguNjggOC4yNTMgOC4yNTMgMCAwIDEgMS42NzIgMS4wOWwuMDkuMDc3LjA4OS4wNzhjLjA2LjA1Mi4xMTQuMTA3LjE3MS4xNTkuMDU3LjA1Mi4xMTIuMTA2LjE2Ni4xNi4wNTIuMDU1LjEwNy4xMDcuMTU5LjE2NGE4LjY3MSA4LjY3MSAwIDAgMSAxLjQxIDEuOTc4Yy4wMTIuMDI2LjAyOC4wNTIuMDQuMDc4bC4wNC4wNzguMDc1LjE1NmMuMDIzLjA1MS4wNS4xLjA3LjE1M2wuMDY1LjE1YTguODQ4IDguODQ4IDAgMCAxIC40NSAxLjM0LjE5LjE5IDAgMCAwIC4yMDEuMTQyLjE4Ni4xODYgMCAwIDAgLjE3Mi0uMTg0Yy4wMS0uMjQ2LjAwMi0uNTMyLS4wMjQtLjg1NnoiPjwvcGF0aD4KICA8L3N2Zz4=)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst Grafana = createSupabaseIcon('Grafana', [\n [\n 'path',\n {\n d: 'M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z',\n key: 'hopart',\n },\n ],\n]);\n\nexport default Grafana;\n", + component: React.lazy(() => import('icons/src/icons/grafana')), + import: "import { Grafana } from 'icons'", + svg: "\n \n ", + jsx: "import { Grafana } from \"icons\"\n \n " +}, { name: "home", componentName: "Home", diff --git a/packages/icons/src/icons/datadog.ts b/packages/icons/src/icons/datadog.ts new file mode 100644 index 00000000000..bdce8a6abae --- /dev/null +++ b/packages/icons/src/icons/datadog.ts @@ -0,0 +1,23 @@ +import createSupabaseIcon from '../createSupabaseIcon'; + +/** + * @component @name Datadog + * @description Supabase SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,PHN2ZyBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIwIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgaGVpZ2h0PSIyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xOS41NyAxNy4wNGwtMS45OTctMS4zMTYtMS42NjUgMi43ODItMS45MzctLjU2Ny0xLjcwNiAyLjYwNC4wODcuODIgOS4yNzQtMS43MS0uNTM4LTUuNzk0em0tOC42NDktMi40OThsMS40ODgtLjIwNGMuMjQxLjEwOC40MDkuMTUuNjk3LjIyMy40NS4xMTcuOTcuMjMgMS43NDEtLjE2LjE4LS4wODguNTUzLS40My43MDQtLjYyNWw2LjA5Ni0xLjEwNi42MjIgNy41MjctMTAuNDQ0IDEuODgyem0xMS4zMjUtMi43MTJsLS42MDIuMTE1TDIwLjQ4OCAwIC43ODkgMi4yODVsMi40MjcgMTkuNjkzIDIuMzA2LS4zMzRjLS4xODQtLjI2My0uNDcxLS41ODEtLjk2LS45ODktLjY4LS41NjQtLjQ0LTEuNTIyLS4wMzktMi4xMjcuNTMtMS4wMjIgMy4yNi0yLjMyMiAzLjEwNi0zLjk1Ni0uMDU2LS41OTQtLjE1LTEuMzY4LS43MDItMS44OTgtLjAyLjIyLjAxNy40MzIuMDE3LjQzMnMtLjIyNy0uMjg5LS4zNC0uNjgzYy0uMTEyLS4xNS0uMi0uMTk5LS4zMTktLjQtLjA4NS4yMzMtLjA3My41MDMtLjA3My41MDNzLS4xODYtLjQzNy0uMjE2LS44MDdjLS4xMS4xNjYtLjEzNy40OC0uMTM3LjQ4cy0uMjQxLS42OS0uMTg2LTEuMDYyYy0uMTEtLjMyMy0uNDM2LS45NjUtLjM0My0yLjQyNC42LjQyMSAxLjkyNC4zMjEgMi40NC0uNDM5LjE3MS0uMjUxLjI4OC0uOTM5LS4wODYtMi4yOTMtLjI0LS44NjgtLjgzNS0yLjE2LTEuMDY2LTIuNjUxbC0uMDI4LjAyYy4xMjIuMzk1LjM3NCAxLjIyMy40NyAxLjYyNS4yOTMgMS4yMTguMzcyIDEuNjQyLjIzNCAyLjIwNC0uMTE2LjQ4OC0uMzk3LjgwOC0xLjEwNyAxLjE2NS0uNzEuMzU4LTEuNjUzLS41MTQtMS43MTMtLjU2Mi0uNjktLjU1LTEuMjI0LTEuNDQ3LTEuMjg0LTEuODgzLS4wNjItLjQ3Ny4yNzUtLjc2My40NDUtMS4xNTMtLjI0My4wNy0uNTE0LjE5Mi0uNTE0LjE5MnMuMzIzLS4zMzQuNzIyLS42MjRjLjE2NS0uMTA5LjI2Mi0uMTc4LjQzNi0uMzIzYTkuNzYyIDkuNzYyIDAgMCAwLS40NTYuMDAzcy40Mi0uMjI3Ljg1NS0uMzkyYy0uMzE4LS4wMTQtLjYyMy0uMDAzLS42MjMtLjAwM3MuOTM3LS40MTkgMS42NzgtLjcyN2MuNTA5LS4yMDggMS4wMDYtLjE0NyAxLjI4Ni4yNTcuMzY3LjUzLjc1Mi44MTcgMS41NjkuOTk2LjUwMS0uMjIzLjY1My0uMzM3IDEuMjg0LS41MDkuNTU0LS42MS45OS0uNjg4Ljk5LS42ODhzLS4yMTYuMTk4LS4yNzQuNTFjLjMxNC0uMjQ5LjY2LS40NTUuNjYtLjQ1NXMtLjEzNC4xNjQtLjI1OS40MjZsLjAzLjA0M2MuMzY2LS4yMi43OTctLjM5NC43OTctLjM5NHMtLjEyMy4xNTYtLjI2OC4zNThjLjI3Ny0uMDAyLjgzOC4wMTIgMS4wNTYuMDM3IDEuMjg1LjAyOCAxLjU1Mi0xLjM3NCAyLjA0NS0xLjU1LjYxOC0uMjIuODk0LS4zNTMgMS45NDcuNjguOTAzLjg4OCAxLjYwOSAyLjQ3NyAxLjI1OSAyLjgzMy0uMjk0LjI5NS0uODc0LS4xMTUtMS41MTYtLjkxNmEzLjQ2NiAzLjQ2NiAwIDAgMS0uNzE2LTEuNTYyIDEuNTMzIDEuNTMzIDAgMCAwLS40OTctLjg1cy4yMy41MS4yMy45NmMwIC4yNDYuMDMgMS4xNjUuNDI0IDEuNjgtLjAzOS4wNzYtLjA1Ny4zNzQtLjEuNDMtLjQ1OC0uNTU0LTEuNDQzLS45NS0xLjYwNC0xLjA2Ny41NDQuNDQ1IDEuNzkzIDEuNDY4IDIuMjczIDIuNDQ5LjQ1My45MjcuMTg2IDEuNzc3LjQxNiAxLjk5Ny4wNjUuMDYzLjk3NiAxLjE5NyAxLjE1IDEuNzY3LjMwNi45OTQuMDE5IDIuMDM4LS4zODEgMi42ODVsLTEuMTE3LjE3NGMtLjE2My0uMDQ1LS4yNzMtLjA2OC0uNDItLjE1My4wOC0uMTQzLjI0MS0uNS4yNDMtLjU3MmwtLjA2My0uMTExYy0uMzQ4LjQ5Mi0uOTMuOTctMS40MTQgMS4yNDUtLjYzMy4zNTktMS4zNjMuMzA0LTEuODM4LjE1Ni0xLjM0OC0uNDE1LTIuNjIzLTEuMzI3LTIuOTMtMS41NjYgMCAwLS4wMS4xOTEuMDQ4LjIzNC4zNC4zODMgMS4xMTkgMS4wNzcgMS44NzIgMS41NmwtMS42MDUuMTc3Ljc1OSA1LjkwOGMtLjMzNy4wNDgtLjM5LjA3MS0uNzU3LjEyNC0uMzI1LTEuMTQ3LS45NDYtMS44OTUtMS42MjQtMi4zMzItLjU5OS0uMzg0LTEuNDI0LS40Ny0yLjIxNC0uMzE0bC0uMDUuMDU5YTIuODUxIDIuODUxIDAgMCAxIDEuODYzLjQ0NGMuNjU0LjQxMyAxLjE4MSAxLjQ4MSAxLjM3NSAyLjEyNC4yNDguODIyLjQyIDEuNy0uMjQ4IDIuNjMyLS40NzYuNjYyLTEuODY0IDEuMDI4LTIuOTg2LjIzNy4zLjQ4MS43MDUuODc2IDEuMjUuOTUuODA5LjExIDEuNTc3LS4wMyAyLjEwNi0uNTc0LjQ1Mi0uNDY0LjY5LTEuNDM0LjYyOC0yLjQ1NmwuNzE0LS4xMDQuMjU4IDEuODM0IDExLjgyNy0xLjQyNHpNMTUuMDUgNi44NDhjLS4wMzQuMDc1LS4wODUuMTI1LS4wMDcuMzdsLjAwNC4wMTQuMDEzLjAzMi4wMzIuMDczYy4xNC4yODcuMjk1LjU1OC41NTIuNjk2LjA2Ny0uMDExLjEzNi0uMDE5LjIwNy0uMDIzLjI0Mi0uMDEuMzk1LjAyOC40OTIuMDguMDA5LS4wNDguMDEtLjExOS4wMDUtLjIyMi0uMDE4LS4zNjQuMDcyLS45ODItLjYyNi0xLjMwOC0uMjY0LS4xMjItLjYzNC0uMDg0LS43NTcuMDY4YS4zMDIuMzAyIDAgMCAxIC4wNTguMDEzYy4xODYuMDY2LjA2LjEzLjAyNy4yMDdtMS45NTggMy4zOTJjLS4wOTItLjA1LS41Mi0uMDMtLjgyMS4wMDUtLjU3NC4wNjgtMS4xOTMuMjY3LTEuMzI4LjM3Mi0uMjQ3LjE5MS0uMTM1LjUyMy4wNDcuNjYuNTExLjM4Mi45Ni42MzggMS40MzIuNTc1LjI5LS4wMzguNTQ2LS40OTcuNzI4LS45MTQuMTI0LS4yODguMTI0LS41OTgtLjA1OC0uNjk4bS01LjA3Ny0yLjk0MmMuMTYyLS4xNTQtLjgwNS0uMzU1LTEuNTU2LjE1Ni0uNTU0LjM3OC0uNTcxIDEuMTg3LS4wNDEgMS42NDYuMDUzLjA0Ni4wOTYuMDc4LjEzNy4xMDRhNC43NyA0Ljc3IDAgMCAxIDEuMzk2LS40MTJjLjExMy0uMTI1LjI0My0uMzQ1LjIxLS43NDUtLjA0NC0uNTQyLS40NTUtLjQ1Ni0uMTQ2LS43NDkiPjwvcGF0aD48L3N2Zz4=) + * + * @param {Object} props - Supabase icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * + */ +const Datadog = createSupabaseIcon('Datadog', [ + [ + 'path', + { + d: 'M19.57 17.04l-1.997-1.316-1.665 2.782-1.937-.567-1.706 2.604.087.82 9.274-1.71-.538-5.794zm-8.649-2.498l1.488-.204c.241.108.409.15.697.223.45.117.97.23 1.741-.16.18-.088.553-.43.704-.625l6.096-1.106.622 7.527-10.444 1.882zm11.325-2.712l-.602.115L20.488 0 .789 2.285l2.427 19.693 2.306-.334c-.184-.263-.471-.581-.96-.989-.68-.564-.44-1.522-.039-2.127.53-1.022 3.26-2.322 3.106-3.956-.056-.594-.15-1.368-.702-1.898-.02.22.017.432.017.432s-.227-.289-.34-.683c-.112-.15-.2-.199-.319-.4-.085.233-.073.503-.073.503s-.186-.437-.216-.807c-.11.166-.137.48-.137.48s-.241-.69-.186-1.062c-.11-.323-.436-.965-.343-2.424.6.421 1.924.321 2.44-.439.171-.251.288-.939-.086-2.293-.24-.868-.835-2.16-1.066-2.651l-.028.02c.122.395.374 1.223.47 1.625.293 1.218.372 1.642.234 2.204-.116.488-.397.808-1.107 1.165-.71.358-1.653-.514-1.713-.562-.69-.55-1.224-1.447-1.284-1.883-.062-.477.275-.763.445-1.153-.243.07-.514.192-.514.192s.323-.334.722-.624c.165-.109.262-.178.436-.323a9.762 9.762 0 0 0-.456.003s.42-.227.855-.392c-.318-.014-.623-.003-.623-.003s.937-.419 1.678-.727c.509-.208 1.006-.147 1.286.257.367.53.752.817 1.569.996.501-.223.653-.337 1.284-.509.554-.61.99-.688.99-.688s-.216.198-.274.51c.314-.249.66-.455.66-.455s-.134.164-.259.426l.03.043c.366-.22.797-.394.797-.394s-.123.156-.268.358c.277-.002.838.012 1.056.037 1.285.028 1.552-1.374 2.045-1.55.618-.22.894-.353 1.947.68.903.888 1.609 2.477 1.259 2.833-.294.295-.874-.115-1.516-.916a3.466 3.466 0 0 1-.716-1.562 1.533 1.533 0 0 0-.497-.85s.23.51.23.96c0 .246.03 1.165.424 1.68-.039.076-.057.374-.1.43-.458-.554-1.443-.95-1.604-1.067.544.445 1.793 1.468 2.273 2.449.453.927.186 1.777.416 1.997.065.063.976 1.197 1.15 1.767.306.994.019 2.038-.381 2.685l-1.117.174c-.163-.045-.273-.068-.42-.153.08-.143.241-.5.243-.572l-.063-.111c-.348.492-.93.97-1.414 1.245-.633.359-1.363.304-1.838.156-1.348-.415-2.623-1.327-2.93-1.566 0 0-.01.191.048.234.34.383 1.119 1.077 1.872 1.56l-1.605.177.759 5.908c-.337.048-.39.071-.757.124-.325-1.147-.946-1.895-1.624-2.332-.599-.384-1.424-.47-2.214-.314l-.05.059a2.851 2.851 0 0 1 1.863.444c.654.413 1.181 1.481 1.375 2.124.248.822.42 1.7-.248 2.632-.476.662-1.864 1.028-2.986.237.3.481.705.876 1.25.95.809.11 1.577-.03 2.106-.574.452-.464.69-1.434.628-2.456l.714-.104.258 1.834 11.827-1.424zM15.05 6.848c-.034.075-.085.125-.007.37l.004.014.013.032.032.073c.14.287.295.558.552.696.067-.011.136-.019.207-.023.242-.01.395.028.492.08.009-.048.01-.119.005-.222-.018-.364.072-.982-.626-1.308-.264-.122-.634-.084-.757.068a.302.302 0 0 1 .058.013c.186.066.06.13.027.207m1.958 3.392c-.092-.05-.52-.03-.821.005-.574.068-1.193.267-1.328.372-.247.191-.135.523.047.66.511.382.96.638 1.432.575.29-.038.546-.497.728-.914.124-.288.124-.598-.058-.698m-5.077-2.942c.162-.154-.805-.355-1.556.156-.554.378-.571 1.187-.041 1.646.053.046.096.078.137.104a4.77 4.77 0 0 1 1.396-.412c.113-.125.243-.345.21-.745-.044-.542-.455-.456-.146-.749', + key: '13qbml', + }, + ], +]); + +export default Datadog; diff --git a/packages/icons/src/icons/grafana.ts b/packages/icons/src/icons/grafana.ts new file mode 100644 index 00000000000..25c9cfdf25b --- /dev/null +++ b/packages/icons/src/icons/grafana.ts @@ -0,0 +1,23 @@ +import createSupabaseIcon from '../createSupabaseIcon'; + +/** + * @component @name Grafana + * @description Supabase SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,PHN2ZyAgICBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgICBzdHJva2Utd2lkdGg9IjAiCiAgICByb2xlPSJpbWciCiAgICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgICBoZWlnaHQ9IjI0IgogICAgd2lkdGg9IjI0IgogICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogID4KICAgIDxwYXRoIGQ9Ik0yMy4wMiAxMC41OWE4LjU3OCA4LjU3OCAwIDAgMC0uODYyLTMuMDM0IDguOTExIDguOTExIDAgMCAwLTEuNzg5LTIuNDQ1Yy4zMzctMS4zNDItLjQxMy0yLjUwNS0uNDEzLTIuNTA1LTEuMjkyLS4wOC0yLjExMy40LTIuNDE2LjYyLS4wNTItLjAyLS4xMDItLjA0NC0uMTU0LS4wNjQtLjIyLS4wODktLjQ0Ni0uMTcyLS42NzctLjI0Ny0uMjMxLS4wNzMtLjQ3LS4xNC0uNzExLS4xOTdhOS44NjcgOS44NjcgMCAwIDAtLjg3NS0uMTYxQzE0LjU1Ny43NTMgMTIuOTQgMCAxMi45NCAwYy0xLjgwNCAxLjE0NS0yLjE0NyAyLjc0NC0yLjE0NyAyLjc0NGwtLjAxOC4wOTNjLS4wOTguMDI5LS4yLjA1Ny0uMjk4LjA4OC0uMTM4LjA0Mi0uMjc1LjA5NC0uNDEzLjE0My0uMTM4LjA1NS0uMjc1LjEwNy0uNDEuMTY2YTguODY5IDguODY5IDAgMCAwLTEuNTU3Ljg3bC0uMDYzLS4wMjljLTIuNDk3LS45NTUtNC43MTYuMTk1LTQuNzE2LjE5NS0uMjAzIDIuNjU4Ljk5NiA0LjMzIDEuMjM1IDQuNjM2YTExLjYwOCAxMS42MDggMCAwIDAtLjYwNyAyLjYzNUMxLjYzNiAxMi42NzcuOTUzIDE1LjAxNC45NTMgMTUuMDE0YzEuOTI2IDIuMjE0IDQuMTcxIDIuMzUxIDQuMTcxIDIuMzUxLjAwMy0uMDAyLjAwNi0uMDAyLjAwNi0uMDA1LjI4NS41MDkuNjE1Ljk5NC45ODYgMS40NDYuMTU2LjE5LjMyLjM3MS40ODguNTQ4LS43MDQgMi4wMDkuMDk5IDMuNjguMDk5IDMuNjggMi4xNDQuMDggMy41NTMtLjkzNyAzLjg0OS0xLjE3M2E5Ljc4NCA5Ljc4NCAwIDAgMCAzLjE2NC41MDFoLjA4bC4wNTUtLjAwMy4xMDctLjAwMi4xMDMtLjAwNS4wMDMuMDAyYzEuMDEgMS40NCAyLjc4OCAxLjY0NiAyLjc4OCAxLjY0NiAxLjI2NC0xLjMzMiAxLjMzNy0yLjY1MyAxLjMzNy0yLjk0di0uMDU4YzAtLjAyLS4wMDMtLjAzOS0uMDAzLS4wNi4yNjUtLjE4Ny41Mi0uMzg3Ljc1OC0uNmE3Ljg3NSA3Ljg3NSAwIDAgMCAxLjQxNS0xLjdjMS40My4wODMgMi40MzctLjg4NSAyLjQzNy0uODg1LS4yMzYtMS40OS0xLjA4NS0yLjIxNi0xLjI2NC0yLjM1NGwtLjAxOC0uMDEzLS4wMTYtLjAxM2EuMjE3LjIxNyAwIDAgMS0uMDMxLS4wMmMuMDA4LS4wOTIuMDE2LS4xOC4wMi0uMjcuMDExLS4xNjIuMDE2LS4zMjMuMDE2LS40OHYtLjI1M2wtLjAwNS0uMDk4LS4wMDgtLjEzNWExLjg5MSAxLjg5MSAwIDAgMC0uMDEtLjEzYy0uMDAzLS4wNDItLjAwOC0uMDgzLS4wMTMtLjEyNWwtLjAxNi0uMTI0LS4wMTgtLjEyMmE2LjIxNSA2LjIxNSAwIDAgMC0yLjAzMi0zLjczIDYuMDE1IDYuMDE1IDAgMCAwLTMuMjIyLTEuNDYgNi4yOTIgNi4yOTIgMCAwIDAtLjg1LS4wNDhsLS4xMDcuMDAyaC0uMDYzbC0uMDQ0LjAwMy0uMTA0LjAwOGE0Ljc3NyA0Ljc3NyAwIDAgMC0zLjMzNSAxLjY5NWMtLjMzMi40LS41OTIuODQtLjc2OCAxLjI5N2E0LjU5NCA0LjU5NCAwIDAgMC0uMzEyIDEuODE3bC4wMDMuMDkxYy4wMDUuMDU1LjAwNy4xMS4wMTMuMTY0YTMuNjE1IDMuNjE1IDAgMCAwIC42OTggMS44MiAzLjUzIDMuNTMgMCAwIDAgMS44MjcgMS4yODJjLjMzLjA5OC42Ni4xNC45NzEuMTM3LjAzOSAwIC4wNzggMCAuMTE0LS4wMDJsLjA2My0uMDAzYy4wMiAwIC4wNDEtLjAwMy4wNjItLjAwMy4wMzQtLjAwMi4wNjUtLjAwNy4wOTktLjAxLjAwNyAwIC4wMTgtLjAwMy4wMjgtLjAwM2wuMDMxLS4wMDUuMDYtLjAwOGExLjE4IDEuMTggMCAwIDAgLjExMi0uMDJjLjAzNi0uMDA4LjA3Mi0uMDEzLjEwOS0uMDI0YTIuNjM0IDIuNjM0IDAgMCAwIC45MTQtLjQxNWMuMDI4LS4wMi4wNTYtLjA0MS4wODUtLjA2NWEuMjQ4LjI0OCAwIDAgMCAuMDM5LS4zNS4yNDQuMjQ0IDAgMCAwLS4zMDktLjA2bC0uMDc4LjA0MmMtLjA5LjA0NC0uMTg0LjA4My0uMjgzLjExNmEyLjQ3NiAyLjQ3NiAwIDAgMS0uNDc1LjA5NmMtLjAyOC4wMDMtLjA1NC4wMDYtLjA4My4wMDZsLS4wODMuMDAyYy0uMDI2IDAtLjA1NCAwLS4wOC0uMDAybC0uMTAyLS4wMDZoLS4wMTJsLS4wMjQuMDA2Yy0uMDE2LS4wMDMtLjAzMS0uMDAzLS4wNDQtLjAwNi0uMDMxLS4wMDItLjA2LS4wMDctLjA5MS0uMDFhMi41OSAyLjU5IDAgMCAxLS43MjQtLjIxMyAyLjU1NyAyLjU1NyAwIDAgMS0uNjY3LS40MzggMi41MiAyLjUyIDAgMCAxLS44MDUtMS40NzUgMi4zMDYgMi4zMDYgMCAwIDEtLjAyOS0uNDQ0bC4wMDYtLjEyMnYtLjAyM2wuMDAyLS4wMzFjLjAwMy0uMDIxLjAwMy0uMDQuMDA1LS4wNmEzLjE2MyAzLjE2MyAwIDAgMSAxLjM1Mi0yLjI5IDMuMTIgMy4xMiAwIDAgMSAuOTM3LS40MyAyLjk0NiAyLjk0NiAwIDAgMSAuNzc2LS4xMDFoLjA2bC4wNy4wMDIuMDQ1LjAwM2guMDI2bC4wNy4wMDVhNC4wNDEgNC4wNDEgMCAwIDEgMS42MzUuNDkgMy45NCAzLjk0IDAgMCAxIDEuNjAyIDEuNjYyIDMuNzcgMy43NyAwIDAgMSAuMzk3IDEuNDE0bC4wMDUuMDc2LjAwMy4wNzVjLjAwMi4wMjYuMDAyLjA1LjAwMi4wNzUgMCAuMDI0LjAwMy4wNTIgMCAuMDd2LjA2NWwtLjAwMi4wNzMtLjAwOC4xNzRhNi4xOTUgNi4xOTUgMCAwIDEtLjA4LjYzOSA1LjEgNS4xIDAgMCAxLS4yNjcuOTI3IDUuMzEgNS4zMSAwIDAgMS0uNjI0IDEuMTMgNS4wNTIgNS4wNTIgMCAwIDEtMy4yMzcgMi4wMTQgNC44MiA0LjgyIDAgMCAxLS42NDkuMDY2bC0uMDM5LjAwM2gtLjI4N2E2LjYwNyA2LjYwNyAwIDAgMS0xLjcxNi0uMjY1IDYuNzc2IDYuNzc2IDAgMCAxLTMuNC0yLjI3NCA2Ljc1IDYuNzUgMCAwIDEtLjc0Ni0xLjE1IDYuNjE2IDYuNjE2IDAgMCAxLS43MTQtMi41OTZsLS4wMDUtLjA4My0uMDAyLS4wMnYtLjA1NmwtLjAwMy0uMDczdi0uMDk2bC0uMDAzLS4xMDR2LS4wN2wuMDAzLS4xNjNjLjAwOC0uMjIuMDI2LS40NS4wNTQtLjY3OGE4LjcwNyA4LjcwNyAwIDAgMSAuMjgtMS4zNTVjLjEyOC0uNDQ0LjI4Ni0uODcyLjQ3My0xLjI3N2E3LjA0IDcuMDQgMCAwIDEgMS40NTYtMi4xIDUuOTI1IDUuOTI1IDAgMCAxIC45NTMtLjc2M2MuMTY5LS4xMTEuMzQzLS4yMTMuNTI0LS4zMDYuMDg5LS4wNS4xODItLjA5MS4yNzMtLjEzNS4wNDctLjAyLjA5My0uMDQyLjEzOC0uMDYyYTcuMTc3IDcuMTc3IDAgMCAxIC43MTQtLjI2N2wuMTQ1LS4wNDVjLjA0OS0uMDE1LjA5OC0uMDI2LjE0OC0uMDQxLjA5OC0uMDI5LjE5Ny0uMDUyLjI5Ni0uMDc2LjA0OS0uMDEzLjEtLjAyLjE1LS4wMzNsLjE1LS4wMzIuMTUxLS4wMjguMDc2LS4wMTMuMDc1LS4wMS4xNTMtLjAyNGMuMDU3LS4wMS4xMTQtLjAxMy4xNzEtLjAyM2wuMTY5LS4wMjFjLjAzNi0uMDAzLjA3My0uMDA4LjEwNi0uMDFsLjA3My0uMDA4LjAzNi0uMDAzLjA0Mi0uMDAyYy4wNTctLjAwMy4xMTQtLjAwOC4xNzEtLjAxbC4wODYtLjAwNmguMDIzbC4wMzctLjAwMy4xNDUtLjAwN2E3Ljk5OSA3Ljk5OSAwIDAgMSAxLjcwOC4xMjUgNy45MTcgNy45MTcgMCAwIDEgMi4wNDguNjggOC4yNTMgOC4yNTMgMCAwIDEgMS42NzIgMS4wOWwuMDkuMDc3LjA4OS4wNzhjLjA2LjA1Mi4xMTQuMTA3LjE3MS4xNTkuMDU3LjA1Mi4xMTIuMTA2LjE2Ni4xNi4wNTIuMDU1LjEwNy4xMDcuMTU5LjE2NGE4LjY3MSA4LjY3MSAwIDAgMSAxLjQxIDEuOTc4Yy4wMTIuMDI2LjAyOC4wNTIuMDQuMDc4bC4wNC4wNzguMDc1LjE1NmMuMDIzLjA1MS4wNS4xLjA3LjE1M2wuMDY1LjE1YTguODQ4IDguODQ4IDAgMCAxIC40NSAxLjM0LjE5LjE5IDAgMCAwIC4yMDEuMTQyLjE4Ni4xODYgMCAwIDAgLjE3Mi0uMTg0Yy4wMS0uMjQ2LjAwMi0uNTMyLS4wMjQtLjg1NnoiPjwvcGF0aD4KICA8L3N2Zz4=) + * + * @param {Object} props - Supabase icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * + */ +const Grafana = createSupabaseIcon('Grafana', [ + [ + 'path', + { + d: 'M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z', + key: 'hopart', + }, + ], +]); + +export default Grafana; diff --git a/packages/icons/src/icons/index.ts b/packages/icons/src/icons/index.ts index 55051580381..4eab2e678fa 100644 --- a/packages/icons/src/icons/index.ts +++ b/packages/icons/src/icons/index.ts @@ -1,19 +1,22 @@ -export { default as RESTApi } from './REST-api' -export { default as ExampleTemplate } from './_example-template' -export { default as ApiDocs } from './api-docs' -export { default as Auth } from './auth' -export { default as Database } from './database' -export { default as EdgeFunctions } from './edge-functions' -export { default as Home } from './home' -export { default as InsertCode } from './insert-code' -export { default as Integrations } from './integrations' -export { default as Logs } from './logs' -export { default as Postgres } from './postgres' -export { default as Realtime } from './realtime' -export { default as ReplaceCode } from './replace-code' -export { default as Reports } from './reports' -export { default as Settings } from './settings' -export { default as SqlEditor } from './sql-editor' -export { default as Storage } from './storage' -export { default as TableEditor } from './table-editor' -export { default as User } from './user' +export { default as RESTApi } from './REST-api'; +export { default as ExampleTemplate } from './_example-template'; +export { default as ApiDocs } from './api-docs'; +export { default as Auth } from './auth'; +export { default as Database } from './database'; +export { default as Datadog } from './datadog'; +export { default as EdgeFunctions } from './edge-functions'; +export { default as Grafana } from './grafana'; +export { default as Home } from './home'; +export { default as InsertCode } from './insert-code'; +export { default as Integrations } from './integrations'; +export { default as Logs } from './logs'; +export { default as Postgres } from './postgres'; +export { default as Realtime } from './realtime'; +export { default as ReplaceCode } from './replace-code'; +export { default as Reports } from './reports'; +export { default as Settings } from './settings'; +export { default as SqlEditor } from './sql-editor'; +export { default as Storage } from './storage'; +export { default as TableEditor } from './table-editor'; +export { default as User } from './user'; + diff --git a/packages/icons/src/raw-icons/datadog.svg b/packages/icons/src/raw-icons/datadog.svg new file mode 100644 index 00000000000..fb0a6f9b643 --- /dev/null +++ b/packages/icons/src/raw-icons/datadog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/icons/src/raw-icons/grafana.svg b/packages/icons/src/raw-icons/grafana.svg new file mode 100644 index 00000000000..d82a970009c --- /dev/null +++ b/packages/icons/src/raw-icons/grafana.svg @@ -0,0 +1,12 @@ + + + \ No newline at end of file From e6c249d9b506a1cffb72831ed3e6df3cf26dc131 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 23 Oct 2024 20:02:20 +0800 Subject: [PATCH 30/80] Shift all uses of project api query to project settings v2 query, and deprecate project api query (#30029) * Add perms check in projects API query * Start deprecating use of projects-api-query, move to projects-settings-v2-query * PRETTY * Shift all uses of project api query to project settings v2 query, and deprecate project api query * Fix * Fix * Small unrelated styling fix on join page * Fix * Update apps/studio/components/interfaces/Auth/Users/UserOverview.tsx Co-authored-by: Alaister Young --------- Co-authored-by: Alaister Young --- .../FunctionsAssistantPreview.tsx | 5 +- .../Auth/AuthProvidersForm/ProviderForm.tsx | 10 +-- .../interfaces/Auth/Users/UserOverview.tsx | 18 ++++-- .../Database/Hooks/HTTPRequestFields.tsx | 13 ++-- .../interfaces/Docs/Introduction.tsx | 3 +- .../EdgeFunctionDetails.tsx | 18 +++--- .../Functions/TerminalInstructions.tsx | 13 ++-- .../Home/NewProjectPanel/APIKeys.tsx | 4 +- .../CronJobs/HttpHeaderFieldsSection.tsx | 9 +-- .../OrganizationInviteError.tsx | 6 +- .../ProjectAPIDocs/ProjectAPIDocs.tsx | 9 +-- .../Inspector/RealtimeTokensPopover/index.tsx | 21 +++---- .../Realtime/Inspector/useRealtimeMessages.ts | 8 +-- .../interfaces/Settings/API/ServiceList.tsx | 6 +- .../CustomDomainActivate.tsx | 8 +-- .../CustomDomainConfig/CustomDomainConfig.tsx | 14 ++--- .../CustomDomainConfig/CustomDomainVerify.tsx | 12 ++-- .../CustomDomainsConfigureHostname.tsx | 8 +-- .../StorageSettings/StorageSettings.utils.ts | 1 - apps/studio/data/config/project-api-query.ts | 62 ------------------- .../[slug]/deploy-button/new-project.tsx | 8 +-- 21 files changed, 97 insertions(+), 159 deletions(-) delete mode 100644 apps/studio/data/config/project-api-query.ts diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx index 3b48059d6bd..513715133e7 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FunctionsAssistantPreview.tsx @@ -1,8 +1,9 @@ +import Image from 'next/image' + import { useParams } from 'common' import { Markdown } from 'components/interfaces/Markdown' import { BASE_PATH } from 'lib/constants' import { detectOS } from 'lib/helpers' -import Image from 'next/image' export const FunctionsAssistantPreview = () => { const os = detectOS() @@ -32,7 +33,7 @@ export const FunctionsAssistantPreview = () => { feature preview proves itself to benefit most of our users, so as usual please do feel free to let us know what you think if the attached GitHub discussion above!

    -
    +

    Enabling this preview will:

    • diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index 69fa28ff76a..57fe2333163 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -1,18 +1,18 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' import { Check, ChevronUp, ExternalLink } from 'lucide-react' +import Link from 'next/link' import { useEffect, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import { toast } from 'sonner' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { components } from 'data/api' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' -import Link from 'next/link' import { Alert, Alert_Shadcn_, @@ -109,8 +109,8 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => { } } - const { data: settings } = useProjectApiQuery({ projectRef }) - const apiUrl = `${settings?.autoApiService.protocol}://${settings?.autoApiService.endpoint}` + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const apiUrl = `https://${settings?.app_config?.endpoint}` const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 0facbc23093..692ba172806 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -16,7 +16,7 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation' import { useUserUpdateMutation } from 'data/auth/user-update-mutation' import { User } from 'data/auth/users-query' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { timeout } from 'lib/helpers' @@ -76,7 +76,8 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { const [isDeleteFactorsModalOpen, setIsDeleteFactorsModalOpen] = useState(false) const { data } = useAuthConfigQuery({ projectRef }) - const { data: apiData } = useProjectApiQuery({ projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const mailerOtpExpiry = data?.MAILER_OTP_EXP ?? 0 const minutes = Math.floor(mailerOtpExpiry / 60) const seconds = Math.floor(mailerOtpExpiry % 60) @@ -142,16 +143,23 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { } const handleUnban = () => { - if (!apiData) { + if (!settings) { return toast.error(`Failed to ban user: Error loading project config`) } else if (user.id === undefined) { return toast.error(`Failed to ban user: User ID not found`) } - const { protocol, endpoint, serviceApiKey } = apiData.autoApiService + const endpoint = settings.app_config?.endpoint + const serviceApiKey = (settings?.service_api_keys ?? []).find( + (key) => key.tags === 'service_role' + )?.api_key + + if (!endpoint) return toast.error(`Failed to unban user: Unable to retrieve API endpoint`) + if (!serviceApiKey) return toast.error(`Failed to unban user: Unable to retrieve API key`) + updateUser({ projectRef, - protocol, + protocol: 'https', endpoint, serviceApiKey, userId: user.id, diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx index 53103b49630..b0077027af9 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx @@ -1,10 +1,11 @@ import clsx from 'clsx' +import { ChevronDown, Plus, Trash } from 'lucide-react' import Link from 'next/link' -import { useParams } from 'common/hooks' +import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { uuidv4 } from 'lib/helpers' import { @@ -19,7 +20,6 @@ import { SidePanel, } from 'ui' import { HTTPArgument } from './EditHookPanel' -import { Trash, Plus, ChevronDown } from 'lucide-react' interface HTTPRequestFieldsProps { type: 'http_request' | 'supabase_function' @@ -48,12 +48,13 @@ const HTTPRequestFields = ({ }: HTTPRequestFieldsProps) => { const { project: selectedProject } = useProjectContext() const { ref } = useParams() - const { data: settings } = useProjectApiQuery({ projectRef: ref }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref }) const edgeFunctions = functions ?? [] - const apiService = settings?.autoApiService - const apiKey = apiService?.serviceApiKey ?? '[YOUR API KEY]' + const apiKey = + (settings?.service_api_keys ?? []).find((key) => key.tags === 'service_role')?.api_key ?? + '[YOUR API KEY]' return ( <> diff --git a/apps/studio/components/interfaces/Docs/Introduction.tsx b/apps/studio/components/interfaces/Docs/Introduction.tsx index b274bc6f388..ecff98bb356 100644 --- a/apps/studio/components/interfaces/Docs/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Introduction.tsx @@ -1,11 +1,10 @@ import { useParams } from 'common' import Snippets from 'components/interfaces/Docs/Snippets' -import type { AutoApiService } from 'data/config/project-api-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import CodeSnippet from './CodeSnippet' import PublicSchemaNotEnabledAlert from './PublicSchemaNotEnabledAlert' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' interface Props { selectedLang: 'bash' | 'js' diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 73bb231b853..671ff7657c1 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -1,6 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import clsx from 'clsx' -import { useParams } from 'common' import dayjs from 'dayjs' import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react' import Link from 'next/link' @@ -8,12 +7,13 @@ import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormActions } from 'components/ui/Forms/FormActions' import { FormHeader } from 'components/ui/Forms/FormHeader' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' @@ -39,7 +39,7 @@ const EdgeFunctionDetails = () => { const [showDeleteModal, setShowDeleteModal] = useState(false) const [showInstructions, setShowInstructions] = useState(false) - const { data: settings } = useProjectApiQuery({ projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug }) const { mutate: updateEdgeFunction, isLoading: isUpdating } = useEdgeFunctionUpdateMutation() @@ -53,15 +53,15 @@ const EdgeFunctionDetails = () => { const formId = 'edge-function-update-form' const canUpdateEdgeFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') - // Get the API service - const apiService = settings?.autoApiService - const anonKey = apiService?.defaultApiKey ?? '[YOUR ANON KEY]' + const anonKey = + (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? + '[YOUR ANON KEY]' - const endpoint = apiService?.app_config.endpoint ?? '' + const endpoint = settings?.app_config?.endpoint ?? '' const functionUrl = customDomainData?.customDomain?.status === 'active' - ? `${apiService?.protocol}://${customDomainData.customDomain.hostname}/functions/v1/${selectedFunction?.slug}` - : `${apiService?.protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}` + ? `https://${customDomainData.customDomain.hostname}/functions/v1/${selectedFunction?.slug}` + : `https://${endpoint}/functions/v1/${selectedFunction?.slug}` const { managementCommands, secretCommands, invokeCommands } = generateCLICommands( selectedFunction, diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index 6ae3158ed18..0ce1ee4ad30 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -5,7 +5,7 @@ import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'reac import { useParams } from 'common' import CommandRender from 'components/interfaces/Functions/CommandRender' import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { Button, @@ -29,19 +29,20 @@ const TerminalInstructions = forwardRef< const [showInstructions, setShowInstructions] = useState(!closable) const { data: tokens } = useAccessTokensQuery() - const { data: settings } = useProjectApiQuery({ projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) - const apiService = settings?.autoApiService - const anonKey = apiService?.defaultApiKey ?? '[YOUR ANON KEY]' - const endpoint = settings?.autoApiService.app_config?.endpoint ?? '' + const anonKey = + (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? + '[YOUR ANON KEY]' + const endpoint = settings?.app_config?.endpoint ?? '' const functionsEndpoint = customDomainData?.customDomain?.status === 'active' ? `https://${customDomainData.customDomain.hostname}/functions/v1` : `https://${endpoint}/functions/v1` // get the .co or .net TLD from the restUrl - const restUrl = settings?.autoApiService.restUrl + const restUrl = `https://${endpoint}` const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' const commands: Commands[] = [ diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index afccbce6923..bfbee79906d 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -1,15 +1,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' +import { AlertCircle, Loader } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' -import { useParams } from 'common/hooks' +import { useParams } from 'common' import Panel from 'components/ui/Panel' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { AlertCircle, Loader } from 'lucide-react' import { Input } from 'ui' const generateInitSnippet = (endpoint: string) => ({ diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx index c44d3bc3eba..cc3c1bade5f 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx @@ -3,7 +3,7 @@ import { useFieldArray } from 'react-hook-form' import { useParams } from 'common' import { FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { Button, cn, @@ -32,9 +32,10 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp }) const { ref } = useParams() - const { data: settings } = useProjectApiQuery({ projectRef: ref }) - const apiService = settings?.autoApiService - const apiKey = apiService?.serviceApiKey ?? '[YOUR API KEY]' + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) + const apiKey = + (settings?.service_api_keys ?? []).find((key) => key.tags === 'service_role') ?? + '[YOUR API KEY]' return ( diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx index b746aa7fb69..cf757202fc9 100644 --- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx @@ -24,16 +24,16 @@ export const OrganizationInviteError = ({ data, error, isError }: OrganizationIn ) : data?.token_does_not_exist ? ( - <> +

      The invite token is invalid.

      Try copying and pasting the link from the invite email, or ask the organization owner to invite you again.

      - +
      ) : !data?.email_match ? (

      diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx index fd6a143b758..3fbacbc4651 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { Button, SidePanel } from 'ui' import { useParams } from 'common' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useAppStateSnapshot } from 'state/app-state' import { @@ -42,16 +42,17 @@ const ProjectAPIDocs = () => { const [showKeys, setShowKeys] = useState(false) const language = snap.docsLanguage - const { data } = useProjectApiQuery({ projectRef: ref }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) const apikey = showKeys - ? data?.autoApiService.defaultApiKey ?? 'SUPABASE_CLIENT_ANON_KEY' + ? (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? + 'SUPABASE_CLIENT_ANON_KEY' : 'SUPABASE_CLIENT_ANON_KEY' const endpoint = customDomainData?.customDomain?.status === 'active' ? `https://${customDomainData.customDomain?.hostname}` - : `https://${data?.autoApiService.endpoint ?? ''}` + : `https://${settings?.app_config?.endpoint ?? ''}` return ( { - const { data: settings } = useProjectApiQuery({ projectRef: config.projectRef }) + const snap = useRoleImpersonationStateSnapshot() - const { mutate: sendEvent } = useSendEventMutation() + const { data: settings } = useProjectSettingsV2Query({ projectRef: config.projectRef }) + const anonRoleKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'anon')?.api_key + const serviceRoleKey = (settings?.service_api_keys ?? []).find( + (x) => x.tags === 'service_role' + )?.api_key - const apiService = settings?.autoApiService - const serviceRoleKey = apiService?.service_api_keys.find((x) => x.name === 'service_role key') - ? apiService.serviceApiKey - : undefined - - const anonRoleKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') - ? apiService.defaultApiKey - : undefined const { data: postgrestConfig } = useProjectPostgrestConfigQuery( { projectRef: config.projectRef, @@ -36,10 +32,11 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens ) const jwtSecret = postgrestConfig?.jwt_secret - const snap = useRoleImpersonationStateSnapshot() + const { mutate: sendEvent } = useSendEventMutation() // only send a telemetry event if the user changes the role. Don't send an event during initial render. const isMounted = useRef(false) + useEffect(() => { if (isMounted.current) { sendEvent({ diff --git a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts index fc785187645..2397b6b1d5d 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts +++ b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts @@ -7,7 +7,7 @@ import { merge, sortBy, take } from 'lodash' import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react' import { toast } from 'sonner' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { uuidv4 } from 'lib/helpers' import { EMPTY_ARR } from 'lib/void' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' @@ -76,11 +76,11 @@ export const useRealtimeMessages = ( enableBroadcast, } = config - const { data } = useProjectApiQuery({ projectRef: projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: projectRef }) // the default host is prod until the correct one comes through an API call. - const host = data - ? `${data.autoApiService.protocol}://${data.autoApiService.endpoint}` + const host = settings + ? `https://${settings.app_config?.endpoint}` : `https://${projectRef}.supabase.co` const realtimeUrl = `${host}/realtime/v1`.replace(/^http/i, 'ws') diff --git a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx index 2f338814316..ae1f0c621c3 100644 --- a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx +++ b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx @@ -12,7 +12,6 @@ import { DisplayApiSettings } from 'components/ui/ProjectSettings' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { configKeys } from 'data/config/keys' -import { useProjectApiQuery } from 'data/config/project-api-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' @@ -29,11 +28,8 @@ const ServiceList = () => { const { ref: projectRef, source } = useParams() const state = useDatabaseSelectorStateSnapshot() - const { data: settings, isError } = useProjectApiQuery({ - projectRef, - }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) - const { data: databases } = useReadReplicasQuery({ projectRef }) + const { data: databases, isError } = useReadReplicasQuery({ projectRef }) const { data: loadBalancers } = useLoadBalancersQuery({ projectRef }) const { data } = useJwtSecretUpdatingStatusQuery({ projectRef }) diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx index eee3f944a8b..000a387666a 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx @@ -1,16 +1,16 @@ +import { AlertCircle, ExternalLink } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' import Panel from 'components/ui/Panel' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckCNAMERecordMutation } from 'data/custom-domains/check-cname-mutation' import { useCustomDomainActivateMutation } from 'data/custom-domains/custom-domains-activate-mutation' import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { AlertCircle, ExternalLink } from 'lucide-react' export type CustomDomainActivateProps = { projectRef?: string @@ -20,7 +20,7 @@ export type CustomDomainActivateProps = { const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => { const [isActivateConfirmModalVisible, setIsActivateConfirmModalVisible] = useState(false) - const { data: settings } = useProjectApiQuery({ projectRef }) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { mutate: checkCNAMERecord, isLoading: isCheckingRecord } = useCheckCNAMERecordMutation() const { mutate: activateCustomDomain, isLoading: isActivating } = useCustomDomainActivateMutation( { @@ -32,7 +32,7 @@ const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivate ) const { mutate: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation() - const endpoint = settings?.autoApiService.endpoint + const endpoint = settings?.app_config?.endpoint const onActivateCustomDomain = async () => { if (!projectRef) return console.error('Project ref is required') diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index bcde78bace3..fcfe9c4f4f3 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -5,7 +5,7 @@ import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms/FormHeader' import Panel from 'components/ui/Panel' import UpgradeToPro from 'components/ui/UpgradeToPro' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' @@ -23,11 +23,9 @@ const CustomDomainConfig = () => { const customDomainsDisabledDueToQuota = useFlag('customDomainsDisabledDueToQuota') const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) - const plan = subscription?.plan?.id - const { isLoading: isSettingsLoading, data: settings } = useProjectApiQuery({ - projectRef: ref, - }) + + const { isLoading: isSettingsLoading } = useProjectSettingsV2Query({ projectRef: ref }) const { isLoading: isCustomDomainsLoading, @@ -101,11 +99,7 @@ const CustomDomainConfig = () => { {(data.status === '1_not_started' || data.status === '2_initiated' || data.status === '3_challenge_verified') && ( - + )} {data.status === '4_origin_setup_completed' && ( diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx index 244664f456d..fe20e53a8dd 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx @@ -2,8 +2,9 @@ import { AlertCircle, ExternalLink, HelpCircle, RefreshCw } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' +import { useParams } from 'common' import Panel from 'components/ui/Panel' -import type { ProjectApiResponse } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' import { useCustomDomainReverifyMutation } from 'data/custom-domains/custom-domains-reverify-mutation' @@ -19,14 +20,15 @@ import DNSRecord from './DNSRecord' import { DNSTableHeaders } from './DNSTableHeaders' export type CustomDomainVerifyProps = { - projectRef?: string customDomain: CustomDomainResponse - settings?: ProjectApiResponse } -const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomainVerifyProps) => { +const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { + const { ref: projectRef } = useParams() const [isNotVerifiedYet, setIsNotVerifiedYet] = useState(false) + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const { mutate: reverifyCustomDomain, isLoading: isReverifyLoading } = useCustomDomainReverifyMutation({ onSuccess: (res) => { @@ -152,7 +154,7 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain )} diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx index 23105004928..1eb5b4536ed 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx @@ -1,4 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import * as yup from 'yup' @@ -7,12 +8,11 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { FormActions } from 'components/ui/Forms/FormActions' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckCNAMERecordMutation } from 'data/custom-domains/check-cname-mutation' import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domains-create-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Form, Input } from 'ui' -import { ExternalLink } from 'lucide-react' const schema = yup.object({ domain: yup.string().required('A value for your custom domain is required'), @@ -24,10 +24,10 @@ const CustomDomainsConfigureHostname = () => { const { mutate: checkCNAMERecord, isLoading: isCheckingRecord } = useCheckCNAMERecordMutation() const { mutate: createCustomDomain, isLoading: isCreating } = useCustomDomainCreateMutation() - const { data: settings } = useProjectApiQuery({ projectRef: ref }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) const FORM_ID = 'custom-domains-form' - const endpoint = settings?.autoApiService.endpoint + const endpoint = settings?.app_config?.endpoint const canConfigureCustomDomain = useCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts index 3dab5cc0cd4..d04aa865ea0 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils.ts @@ -1,4 +1,3 @@ -import { ProjectApiResponse } from 'data/config/project-api-query' import { StorageSizeUnits } from './StorageSettings.constants' const k = 1024 diff --git a/apps/studio/data/config/project-api-query.ts b/apps/studio/data/config/project-api-query.ts deleted file mode 100644 index 3826300f630..00000000000 --- a/apps/studio/data/config/project-api-query.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' - -import { components } from 'api-types' -import { get, handleError } from 'data/fetchers' -import { useProjectByRef } from 'hooks/misc/useSelectedProject' -import { PROJECT_STATUS } from 'lib/constants' -import type { ResponseError } from 'types' -import { configKeys } from './keys' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { PermissionAction } from '@supabase/shared-types/out/constants' - -export type ProjectApiVariables = { - projectRef?: string -} - -export type AutoApiService = components['schemas']['AutoApiService'] - -export type ProjectApiResponse = { - autoApiService: AutoApiService -} - -export async function getProjectApi({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { - if (!projectRef) throw new Error('projectRef is required') - - const { data, error } = await get('/platform/props/project/{ref}/api', { - params: { path: { ref: projectRef } }, - signal, - }) - - if (error) handleError(error) - return data -} - -export type ProjectApiData = Awaited> -export type ProjectApiError = ResponseError - -/** @deprecated */ -export const useProjectApiQuery = ( - { projectRef }: ProjectApiVariables, - { enabled = true, ...options }: UseQueryOptions = {} -) => { - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') - - return useQuery( - configKeys.api(projectRef), - ({ signal }) => getProjectApi({ projectRef }, signal), - { - enabled: enabled && typeof projectRef !== 'undefined', - refetchInterval(data) { - if (!data) return false - - const { autoApiService } = data as unknown as ProjectApiData - - const apiKeys = autoApiService?.service_api_keys ?? [] - const interval = canReadAPIKeys && apiKeys.length === 0 ? 2000 : 0 - - return interval - }, - ...options, - } - ) -} diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index 0334469cb30..aa0903d6141 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -10,7 +10,7 @@ import { Markdown } from 'components/interfaces/Markdown' import VercelIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout' import { ScaffoldColumn, ScaffoldContainer } from 'components/layouts/Scaffold' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useIntegrationsQuery } from 'data/integrations/integrations-query' import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation' import { useIntegrationVercelConnectionsCreateMutation } from 'data/integrations/integrations-vercel-connections-create-mutation' @@ -165,16 +165,16 @@ const CreateProject = () => { } // Wait for the new project to be created before creating the connection - useProjectApiQuery( + useProjectSettingsV2Query( { projectRef: newProjectRef }, { enabled: newProjectRef !== undefined, // refetch until the project is created refetchInterval: (data) => { - return (data?.autoApiService.service_api_keys.length ?? 0) > 0 ? false : 1000 + return ((data?.service_api_keys ?? []).length ?? 0) > 0 ? false : 1000 }, async onSuccess(data) { - const isReady = data.autoApiService.service_api_keys.length > 0 + const isReady = (data?.service_api_keys ?? []).length > 0 if (!isReady || !organizationIntegration || !foreignProjectId || !newProjectRef) { return From 361d429c9f31d3d8f0b329f6ada34558cdb074b7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 23 Oct 2024 21:09:34 +0800 Subject: [PATCH 31/80] Shift all uses of project-settings-query to project-settings-v2-query and deprecate project-settings-query (#30031) * Add perms check in projects API query * Start deprecating use of projects-api-query, move to projects-settings-v2-query * PRETTY * Shift all uses of project api query to project settings v2 query, and deprecate project api query * Shift all uses of project-settings-query to project-settings-v2-query and deprecate project-settings-query * Fix --- .../interfaces/Auth/Users/BanUserModal.tsx | 10 ++- .../interfaces/Auth/Users/CreateUserModal.tsx | 17 +++-- .../interfaces/Auth/Users/UserOverview.tsx | 10 ++- .../Database/Hooks/HTTPRequestFields.tsx | 7 +- .../EdgeFunctionDetails.tsx | 9 ++- .../Functions/TerminalInstructions.tsx | 9 ++- .../Home/Connect/ConnectTabContent.tsx | 12 ++-- .../Home/NewProjectPanel/APIKeys.tsx | 8 +-- .../CronJobs/HttpHeaderFieldsSection.tsx | 7 +- .../ProjectAPIDocs/ProjectAPIDocs.tsx | 6 +- .../Inspector/RealtimeTokensPopover/index.tsx | 13 ++-- .../interfaces/Settings/Addons/Addons.tsx | 14 ++-- .../Settings/Database/SSLConfiguration.tsx | 7 +- .../Settings/Database/UsePoolerCheckbox.tsx | 6 +- .../layouts/StorageLayout/StorageLayout.tsx | 12 ++-- .../Storage/StorageExplorer/useCopyUrl.tsx | 11 +-- .../ui/ProjectSettings/DisplayApiSettings.tsx | 17 ++--- .../ProjectSettings/DisplayConfigSettings.tsx | 30 ++++---- .../data/config/project-settings-query.ts | 69 ------------------- .../project/[ref]/integrations/graphiql.tsx | 10 ++- 20 files changed, 96 insertions(+), 188 deletions(-) delete mode 100644 apps/studio/data/config/project-settings-query.ts diff --git a/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx index d246cd04cd5..cb203fcc064 100644 --- a/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/BanUserModal.tsx @@ -8,7 +8,7 @@ import * as z from 'zod' import { useParams } from 'common' import { useUserUpdateMutation } from 'data/auth/user-update-mutation' import { User } from 'data/auth/users-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { Button, cn, @@ -70,18 +70,16 @@ export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => { const durationHours = data.unit === 'hours' ? Number(data.value) : Number(data.value) * 24 const endpoint = settings.app_config?.endpoint - const serviceApiKey = (settings?.service_api_keys ?? []).find( - (x) => x.tags === 'service_role' - )?.api_key + const { serviceKey } = getAPIKeys(settings) if (!endpoint) return toast.error(`Failed to ban user: Unable to retrieve API endpoint`) - if (!serviceApiKey) return toast.error(`Failed to ban user: Unable to retrieve API key`) + if (!serviceKey?.api_key) return toast.error(`Failed to ban user: Unable to retrieve API key`) updateUser({ projectRef, protocol: 'https', endpoint, - serviceApiKey, + serviceApiKey: serviceKey.api_key, userId: user.id, banDuration: durationHours, }) diff --git a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx index 3f5cdb41567..41a421ddcd6 100644 --- a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx @@ -7,7 +7,7 @@ import * as z from 'zod' import { useParams } from 'common' import { useUserCreateMutation } from 'data/auth/user-create-mutation' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button, @@ -61,14 +61,19 @@ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { return toast.error(`Failed to create user: Error loading project config`) } const endpoint = settings?.app_config?.endpoint - const serviceApiKey = (settings?.service_api_keys ?? []).find( - (key) => key.tags === 'service_role' - )?.api_key + const { serviceKey } = getAPIKeys(settings) if (!endpoint) return toast.error(`Failed to create user: Unable to retrieve API endpoint`) - if (!serviceApiKey) return toast.error(`Failed to create user: Unable to retrieve API key`) + if (!serviceKey?.api_key) + return toast.error(`Failed to create user: Unable to retrieve API key`) - createUser({ projectRef, endpoint, protocol: 'https', serviceApiKey, user: values }) + createUser({ + projectRef, + endpoint, + protocol: 'https', + serviceApiKey: serviceKey.api_key, + user: values, + }) } const form = useForm>({ diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 692ba172806..7dd8d768955 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -16,7 +16,7 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation' import { useUserUpdateMutation } from 'data/auth/user-update-mutation' import { User } from 'data/auth/users-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { timeout } from 'lib/helpers' @@ -150,18 +150,16 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { } const endpoint = settings.app_config?.endpoint - const serviceApiKey = (settings?.service_api_keys ?? []).find( - (key) => key.tags === 'service_role' - )?.api_key + const { serviceKey } = getAPIKeys(settings) if (!endpoint) return toast.error(`Failed to unban user: Unable to retrieve API endpoint`) - if (!serviceApiKey) return toast.error(`Failed to unban user: Unable to retrieve API key`) + if (!serviceKey?.api_key) return toast.error(`Failed to unban user: Unable to retrieve API key`) updateUser({ projectRef, protocol: 'https', endpoint, - serviceApiKey, + serviceApiKey: serviceKey.api_key, userId: user.id, banDuration: 'none', }) diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx index b0077027af9..590a3caeee3 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx @@ -5,7 +5,7 @@ import Link from 'next/link' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { uuidv4 } from 'lib/helpers' import { @@ -52,9 +52,8 @@ const HTTPRequestFields = ({ const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref }) const edgeFunctions = functions ?? [] - const apiKey = - (settings?.service_api_keys ?? []).find((key) => key.tags === 'service_role')?.api_key ?? - '[YOUR API KEY]' + const { serviceKey } = getAPIKeys(settings) + const apiKey = serviceKey?.api_key ?? '[YOUR API KEY]' return ( <> diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 671ff7657c1..3936710c68e 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -13,7 +13,7 @@ import { FormActions } from 'components/ui/Forms/FormActions' import { FormHeader } from 'components/ui/Forms/FormHeader' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' @@ -53,9 +53,8 @@ const EdgeFunctionDetails = () => { const formId = 'edge-function-update-form' const canUpdateEdgeFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') - const anonKey = - (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? - '[YOUR ANON KEY]' + const { anonKey } = getAPIKeys(settings) + const apiKey = anonKey?.api_key ?? '[YOUR ANON KEY]' const endpoint = settings?.app_config?.endpoint ?? '' const functionUrl = @@ -66,7 +65,7 @@ const EdgeFunctionDetails = () => { const { managementCommands, secretCommands, invokeCommands } = generateCLICommands( selectedFunction, functionUrl, - anonKey + apiKey ) const onUpdateFunction = async (values: any, { resetForm }: any) => { diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index 0ce1ee4ad30..1c100782e7f 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -5,7 +5,7 @@ import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'reac import { useParams } from 'common' import CommandRender from 'components/interfaces/Functions/CommandRender' import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { Button, @@ -32,9 +32,8 @@ const TerminalInstructions = forwardRef< const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) - const anonKey = - (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? - '[YOUR ANON KEY]' + const { anonKey } = getAPIKeys(settings) + const apiKey = anonKey?.api_key ?? '[YOUR ANON KEY]' const endpoint = settings?.app_config?.endpoint ?? '' const functionsEndpoint = customDomainData?.customDomain?.status === 'active' @@ -73,7 +72,7 @@ const TerminalInstructions = forwardRef< }, { command: `curl -L -X POST 'https://${projectRef}.supabase.${restUrlTld}/functions/v1/hello-world' -H 'Authorization: Bearer ${ - anonKey ?? '[YOUR ANON KEY]' + apiKey ?? '[YOUR ANON KEY]' }' --data '{"name":"Functions"}'`, description: 'Invokes the hello-world function', jsx: () => { diff --git a/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx index 59eac3804d6..005b806419e 100644 --- a/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx +++ b/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx @@ -1,14 +1,14 @@ -import { useParams } from 'common' import dynamic from 'next/dynamic' import { forwardRef, HTMLAttributes } from 'react' +import { useParams } from 'common' import { getConnectionStrings } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { pluckObjectFields } from 'lib/helpers' -import type { projectKeys } from './Connect.types' import { cn } from 'ui' +import type { projectKeys } from './Connect.types' interface ConnectContentTabProps extends HTMLAttributes { projectKeys: projectKeys @@ -23,13 +23,13 @@ interface ConnectContentTabProps extends HTMLAttributes { const ConnectTabContentNew = forwardRef( ({ projectKeys, filePath, ...props }, ref) => { const { ref: projectRef } = useParams() - const { data } = useProjectSettingsQuery({ projectRef }) + + const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef }) - const { project } = data ?? {} const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } - const connectionInfo = pluckObjectFields(project || emptyState, DB_FIELDS) + const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS) const poolingConfiguration = poolingInfo?.find((x) => x.database_type === 'PRIMARY') const connectionStringsPooler = diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index bfbee79906d..7265f3c38f5 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -55,17 +55,17 @@ const APIKeys = () => { const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') - const apiKeys = settings?.service_api_keys ?? [] - - // API keys should not be empty. However it can be populated with a delay on project creation - const isApiKeysEmpty = apiKeys.length === 0 const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated const endpoint = settings?.app_config?.endpoint const apiUrl = `https://${endpoint ?? '-'}` + const apiKeys = settings?.service_api_keys ?? [] const { anonKey } = getAPIKeys(settings) + // API keys should not be empty. However it can be populated with a delay on project creation + const isApiKeysEmpty = apiKeys.length === 0 + const clientInitSnippet: any = generateInitSnippet(apiUrl) const selectedLanguageSnippet = clientInitSnippet[selectedLanguage.key] ?? 'No snippet available' diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx index cc3c1bade5f..bfd46415d0b 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx @@ -3,7 +3,7 @@ import { useFieldArray } from 'react-hook-form' import { useParams } from 'common' import { FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { Button, cn, @@ -33,9 +33,8 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp const { ref } = useParams() const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) - const apiKey = - (settings?.service_api_keys ?? []).find((key) => key.tags === 'service_role') ?? - '[YOUR API KEY]' + const { serviceKey } = getAPIKeys(settings) + const apiKey = serviceKey?.api_key ?? '[YOUR API KEY]' return ( diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx index 3fbacbc4651..ee8e926145a 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { Button, SidePanel } from 'ui' import { useParams } from 'common' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useAppStateSnapshot } from 'state/app-state' import { @@ -45,9 +45,9 @@ const ProjectAPIDocs = () => { const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) + const { anonKey } = getAPIKeys(settings) const apikey = showKeys - ? (settings?.service_api_keys ?? []).find((key) => key.tags === 'anon')?.api_key ?? - 'SUPABASE_CLIENT_ANON_KEY' + ? anonKey?.api_key ?? 'SUPABASE_CLIENT_ANON_KEY' : 'SUPABASE_CLIENT_ANON_KEY' const endpoint = customDomainData?.customDomain?.status === 'active' diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx index 65c8fa5317c..09a861e5a3c 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx @@ -3,7 +3,7 @@ import { toast } from 'sonner' import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' @@ -19,10 +19,7 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens const snap = useRoleImpersonationStateSnapshot() const { data: settings } = useProjectSettingsV2Query({ projectRef: config.projectRef }) - const anonRoleKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'anon')?.api_key - const serviceRoleKey = (settings?.service_api_keys ?? []).find( - (x) => x.tags === 'service_role' - )?.api_key + const { anonKey, serviceKey } = getAPIKeys(settings) const { data: postgrestConfig } = useProjectPostgrestConfigQuery( { @@ -59,12 +56,12 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens snap.role !== undefined && snap.role.type === 'postgrest' ) { - token = anonRoleKey + token = anonKey?.api_key await getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role) .then((b) => (bearer = b)) .catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`)) } else { - token = serviceRoleKey + token = serviceKey?.api_key } if (token) { onChangeConfig({ ...config, token, bearer }) @@ -72,7 +69,7 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens } triggerUpdateTokenBearer() - }, [snap.role, anonRoleKey, serviceRoleKey]) + }, [snap.role, anonKey, serviceKey]) return } diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 76c0d237bcd..f695cfff4c0 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -1,5 +1,4 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import { useParams } from 'common' import dayjs from 'dayjs' import { AlertCircle, ChevronRight, ExternalLink, Info } from 'lucide-react' import { useTheme } from 'next-themes' @@ -7,6 +6,7 @@ import Image from 'next/image' import Link from 'next/link' import { useMemo } from 'react' +import { useParams } from 'common' import { getAddons, subscriptionHasHipaaAddon, @@ -26,7 +26,7 @@ import { import AlertError from 'components/ui/AlertError' import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { ProjectAddonVariantMeta } from 'data/subscriptions/types' @@ -48,15 +48,15 @@ const Addons = () => { const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() const { setPanel } = useAddonsPagePanel() - const { project: selectedProject, isLoading: isLoadingProject } = useProjectContext() - const { data: projectSettings } = useProjectSettingsQuery({ projectRef }) const selectedOrg = useSelectedOrganization() - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) - + const { project: selectedProject, isLoading: isLoadingProject } = useProjectContext() const parentProject = useProjectByRef(selectedProject?.parent_project_ref) const isBranch = parentProject !== undefined const isProjectActive = useIsProjectActive() + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) + const computeSizeChangesDisabled = useFlag('disableComputeSizeChanges') const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') @@ -105,7 +105,7 @@ const Addons = () => { return computeMeta }, [selectedProject, computeInstance]) - const canUpdateIPv4 = projectSettings?.project.db_ip_addr_config === 'ipv6' + const canUpdateIPv4 = settings?.db_ip_addr_config === 'ipv6' return ( <> diff --git a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index b9621087efb..88a0ac96998 100644 --- a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx @@ -10,7 +10,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormHeader } from 'components/ui/Forms/FormHeader' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSSLEnforcementQuery } from 'data/ssl-enforcement/ssl-enforcement-query' import { useSSLEnforcementUpdateMutation } from 'data/ssl-enforcement/ssl-enforcement-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -27,7 +27,7 @@ const SSLConfiguration = () => { const { ref } = useParams() const [isEnforced, setIsEnforced] = useState(false) - const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) const { data: sslEnforcementConfiguration, isLoading, @@ -65,8 +65,7 @@ const SSLConfiguration = () => { ) const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging' const hasSSLCertificate = - projectSettings?.project !== undefined && - new Date(projectSettings.project.inserted_at) >= new Date('2021-04-30') + settings?.inserted_at !== undefined && new Date(settings.inserted_at) >= new Date('2021-04-30') useEffect(() => { if (!isLoading && sslEnforcementConfiguration) { diff --git a/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx b/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx index 9f163c335db..6990f9f60d0 100644 --- a/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx +++ b/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { useParams } from 'common' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { usePgBouncerStatus } from 'data/config/pgbouncer-enabled-query' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' @@ -51,12 +51,12 @@ export const UsePoolerCheckbox = ({ const state = useDatabaseSelectorStateSnapshot() const { data, isLoading, isSuccess } = usePoolingConfigurationQuery({ projectRef }) - const { data: settings, isSuccess: isSuccessSettings } = useProjectSettingsQuery({ projectRef }) + const { data: settings, isSuccess: isSuccessSettings } = useProjectSettingsV2Query({ projectRef }) const { data: pgBouncerStatus } = usePgBouncerStatus({ projectRef: projectRef }) const isDatabaseSettingsPage = router.pathname.endsWith('/settings/database') const poolingConfiguration = data?.find((x) => x.identifier === state.selectedDatabaseId) - const resolvesToIpV6 = settings?.project.db_ip_addr_config === 'ipv6' + const resolvesToIpV6 = settings?.db_ip_addr_config === 'ipv6' const onSelectOption = (value: 'session' | 'transaction') => { onSelectPoolingMode(value) diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index 0df1670f64f..cacfb57f7e3 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -2,7 +2,7 @@ import { useParams } from 'common' import { ReactNode, useEffect } from 'react' import { toast } from 'sonner' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { PROJECT_STATUS } from 'lib/constants' @@ -22,16 +22,16 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { const { data: settings, isLoading } = useProjectSettingsV2Query({ projectRef }) const endpoint = settings?.app_config?.endpoint - const serviceApiKey = (settings?.service_api_keys ?? []).find( - (key) => key.tags === 'service_role' - )?.api_key + const { serviceKey } = getAPIKeys(settings) const isPaused = project?.status === PROJECT_STATUS.INACTIVE useEffect(() => { - if (!isLoading && endpoint && serviceApiKey) initializeStorageStore(endpoint, serviceApiKey) + if (!isLoading && endpoint && serviceKey?.api_key) { + initializeStorageStore(endpoint, serviceKey.api_key) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading, projectRef, endpoint, serviceApiKey]) + }, [isLoading, projectRef, endpoint, serviceKey?.api_key]) const initializeStorageStore = async (endpoint: string, serviceApiKey: string) => { if (isPaused) return diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx index 925eea0cfdf..7543eb48899 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useCopyUrl.tsx @@ -1,19 +1,14 @@ import { toast } from 'sonner' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants/infrastructure' import { copyToClipboard } from 'lib/helpers' export const useCopyUrl = (ref: string) => { const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) - const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref }) + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) - const apiService = (projectSettings?.services ?? []).find( - (x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID - ) - const apiConfig = apiService?.app_config - const apiUrl = `${apiConfig?.protocol ?? 'https'}://${apiConfig?.endpoint ?? '-'}` + const apiUrl = `https://${settings?.app_config?.endpoint ?? '-'}` const onCopyUrl = (name: string, url: string | Promise) => { const formattedUrl = Promise.resolve(url).then((url) => { diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx index 60df961219c..9cf249a7ca3 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx +++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -1,14 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' -import { Button, Input } from 'ui' +import { AlertCircle, BookOpen, Loader2 } from 'lucide-react' -import { useParams } from 'common/hooks' +import { useParams } from 'common' import Panel from 'components/ui/Panel' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' -import { AlertCircle, BookOpen, Loader2 } from 'lucide-react' +import { Button, Input } from 'ui' const DisplayApiSettings = ({ legacy }: { legacy?: boolean }) => { const { ref: projectRef } = useParams() @@ -17,7 +16,7 @@ const DisplayApiSettings = ({ legacy }: { legacy?: boolean }) => { data: settings, isError: isProjectSettingsError, isLoading: isProjectSettingsLoading, - } = useProjectSettingsQuery({ projectRef }) + } = useProjectSettingsV2Query({ projectRef }) const { data, isError: isJwtSecretUpdateStatusError, @@ -29,11 +28,7 @@ const DisplayApiSettings = ({ legacy }: { legacy?: boolean }) => { const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated - // Get the API service - const apiService = (settings?.services ?? []).find( - (x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID - ) - const apiKeys = apiService?.service_api_keys ?? [] + const apiKeys = settings?.service_api_keys ?? [] // api keys should not be empty. However it can be populated with a delay on project creation const isApiKeysEmpty = apiKeys.length === 0 diff --git a/apps/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx index 55ca8c52fce..cb9929f26f1 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx +++ b/apps/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx @@ -1,13 +1,13 @@ import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' -import { useParams } from 'common' +import { AlertCircle, Loader } from 'lucide-react' import { PropsWithChildren } from 'react' -import { Input } from 'ui' +import { useParams } from 'common' import Panel from 'components/ui/Panel' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' -import { useProjectSettingsQuery } from 'data/config/project-settings-query' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' -import { AlertCircle, Loader } from 'lucide-react' +import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { Input } from 'ui' const DisplayConfigSettings = () => { const { ref: projectRef } = useParams() @@ -15,9 +15,10 @@ const DisplayConfigSettings = () => { data: settings, isLoading: isProjectSettingsLoading, isError: isProjectSettingsError, - } = useProjectSettingsQuery({ + } = useProjectSettingsV2Query({ projectRef, }) + const { data: config, isError: isPostgrestError } = useProjectPostgrestConfigQuery({ projectRef }) const { data, @@ -25,29 +26,24 @@ const DisplayConfigSettings = () => { isLoading: isJwtSecretUpdateStatusLoading, } = useJwtSecretUpdatingStatusQuery({ projectRef }) const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus - const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated - // Get the API service - const jwtSecret = settings?.project?.jwt_secret ?? '' - const apiService = (settings?.services ?? []).find( - (x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID - ) - const apiConfig = apiService?.app_config - const apiUrl = `${apiConfig?.protocol ?? 'https'}://${apiConfig?.endpoint ?? '-'}` + + const jwtSecret = config?.jwt_secret ?? '' + const apiUrl = `https://${settings?.app_config?.endpoint ?? '-'}` return ( - {isProjectSettingsError || isJwtSecretUpdateStatusError ? ( + {isProjectSettingsError || isPostgrestError || isJwtSecretUpdateStatusError ? (

      - {isProjectSettingsError + {isProjectSettingsError || isPostgrestError ? 'Failed to retrieve configuration' : 'Failed to update JWT secret'}

      - ) : isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? ( + ) : isProjectSettingsLoading || isPostgrestError || isJwtSecretUpdateStatusLoading ? (

      diff --git a/apps/studio/data/config/project-settings-query.ts b/apps/studio/data/config/project-settings-query.ts deleted file mode 100644 index 7d9d642c132..00000000000 --- a/apps/studio/data/config/project-settings-query.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' - -import type { components } from 'data/api' -import { get, handleError } from 'data/fetchers' -import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants' -import type { ResponseError } from 'types' -import { configKeys } from './keys' - -export type ProjectSettingsVariables = { - projectRef?: string -} - -// [Joshen] API typing needs to be fixed - this is completely wrong -type ProjectSettingsInfo = components['schemas']['ProjectResponse'] & { db_port: number } -interface ProjectSettingsInfoExtended extends ProjectSettingsInfo { - // [Joshen] Based on the enums here - // https://github.com/supabase/infrastructure/blob/95dc09fe077dba7817bb112fa72b6814a620ecd3/shared/src/projects.ts#L177 - db_ip_addr_config: 'legacy' | 'static-ipv4' | 'concurrent-ipv6' | 'ipv6' -} - -export type ProjectSettings = { - project: ProjectSettingsInfoExtended - services: components['schemas']['SettingsResponse']['services'] -} - -export async function getProjectSettings( - { projectRef }: ProjectSettingsVariables, - signal?: AbortSignal -) { - if (!projectRef) throw new Error('projectRef is required') - - // [Joshen] API typing is wrong here - const { data, error } = await get('/platform/props/project/{ref}/settings', { - params: { path: { ref: projectRef } }, - signal, - }) - - if (error) handleError(error) - return data as unknown as ProjectSettings -} - -export type ProjectSettingsData = Awaited> -export type ProjectSettingsError = ResponseError - -/** @deprecated */ -export const useProjectSettingsQuery = ( - { projectRef }: ProjectSettingsVariables, - { - enabled = true, - ...options - }: UseQueryOptions = {} -) => - useQuery( - configKeys.settings(projectRef), - ({ signal }) => getProjectSettings({ projectRef }, signal), - { - enabled: enabled && typeof projectRef !== 'undefined', - refetchInterval(data) { - const apiService = ((data as ProjectSettings)?.services ?? []).find( - (x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID - ) - const apiKeys = apiService?.service_api_keys ?? [] - const interval = apiKeys.length === 0 ? 2000 : 0 - - return interval - }, - ...options, - } - ) diff --git a/apps/studio/pages/project/[ref]/integrations/graphiql.tsx b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx index 496ba9cb95c..af3804e7554 100644 --- a/apps/studio/pages/project/[ref]/integrations/graphiql.tsx +++ b/apps/studio/pages/project/[ref]/integrations/graphiql.tsx @@ -11,7 +11,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { Loading } from 'components/ui/Loading' import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { API_URL, IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' @@ -34,9 +34,7 @@ const GraphiQLPage: NextPageWithLayout = () => { const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM }) const { data: settings, isFetched } = useProjectSettingsV2Query({ projectRef }) - const serviceRoleKey = (settings?.service_api_keys ?? []).find( - (x) => x.name === 'service_role key' - )?.api_key + const { serviceKey } = getAPIKeys(settings) const { data: config } = useProjectPostgrestConfigQuery({ projectRef }) const jwtSecret = config?.jwt_secret @@ -77,13 +75,13 @@ const GraphiQLPage: NextPageWithLayout = () => { opts?.headers?.['Authorization'] ?? opts?.headers?.['authorization'] ?? userAuthorization ?? - `Bearer ${serviceRoleKey}`, + `Bearer ${serviceKey?.api_key}`, }, }) } return customFetcher - }, [projectRef, getImpersonatedRole, jwtSecret, accessToken, serviceRoleKey]) + }, [projectRef, getImpersonatedRole, jwtSecret, accessToken, serviceKey]) if ((IS_PLATFORM && !accessToken) || !isFetched || (isExtensionsLoading && !pgGraphqlExtension)) { return From b5a515aca613e8d473a69ab0ab59c351c47007d2 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 23 Oct 2024 21:26:09 +0800 Subject: [PATCH 32/80] fix: avatar url startsWith error (#30053) --- apps/studio/components/interfaces/Auth/Users/Users.utils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index 4bb4eaf5024..a1cf50f2c64 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -211,9 +211,9 @@ export function getAvatarUrl(user: User): string | undefined { profileImageUrl || profileImageURL || profile_image_url || - '') as string | undefined + '') as unknown - if (url === undefined) return undefined + if (typeof url !== 'string') return undefined const isSupported = SUPPORTED_CSP_AVATAR_URLS.some((x) => url.startsWith(x)) return isSupported ? url : undefined } From 14076eed865defd5c5a2b802423731cf165cf6a0 Mon Sep 17 00:00:00 2001 From: Mark Burggraf Date: Wed, 23 Oct 2024 09:58:40 -0700 Subject: [PATCH 33/80] added new neon migration guide (#30056) --- .../NavigationMenu.constants.ts | 6 ++ .../platform/migrating-to-supabase/neon.mdx | 90 +++++++++++++++++++ .../docs/public/img/icons/neon-icon-light.svg | 4 + apps/docs/public/img/icons/neon-icon.svg | 4 + 4 files changed, 104 insertions(+) create mode 100644 apps/docs/content/guides/platform/migrating-to-supabase/neon.mdx create mode 100644 apps/docs/public/img/icons/neon-icon-light.svg create mode 100644 apps/docs/public/img/icons/neon-icon.svg diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 6aa8637e1ff..27a91a80c11 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1861,6 +1861,12 @@ export const MIGRATION_PAGES: Partial dump.sql +``` + +2. Import the database to your Supabase project + + Use `psql` to import the Postgres database file to your Supabase project. + + ```bash + psql -d "$NEW_DB_URL" -f dump.sql + ``` + +Additional options + +- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command. +- To exclude a schema: `--exclude-schema=PATTERN`. +- To only migrate a single table: `--table=PATTERN`. +- To exclude a table: `--exclude-table=PATTERN`. + +Run `pg_dump --help` for a full list of options. + + + +## Enterprise + +[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project. diff --git a/apps/docs/public/img/icons/neon-icon-light.svg b/apps/docs/public/img/icons/neon-icon-light.svg new file mode 100644 index 00000000000..c8906afd151 --- /dev/null +++ b/apps/docs/public/img/icons/neon-icon-light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/docs/public/img/icons/neon-icon.svg b/apps/docs/public/img/icons/neon-icon.svg new file mode 100644 index 00000000000..7fb2f931cc4 --- /dev/null +++ b/apps/docs/public/img/icons/neon-icon.svg @@ -0,0 +1,4 @@ + + + + From 438e1f0b6ecc19bfd98b5a0051aab74457f05690 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 24 Oct 2024 03:47:37 +0200 Subject: [PATCH 34/80] remove webinar announcement link (#30059) * remove webinar announcement link * watch now --- .../_events/2024-10-23-scale-to-millions-goodtape-auth.mdx | 2 +- apps/www/components/Hero/Hero.tsx | 4 ---- apps/www/components/Nav/ProductDropdown.tsx | 5 ----- apps/www/pages/customers.tsx | 4 ---- apps/www/public/customers-rss.xml | 2 +- 5 files changed, 2 insertions(+), 15 deletions(-) diff --git a/apps/www/_events/2024-10-23-scale-to-millions-goodtape-auth.mdx b/apps/www/_events/2024-10-23-scale-to-millions-goodtape-auth.mdx index 5c776e45e17..6ec223743a7 100644 --- a/apps/www/_events/2024-10-23-scale-to-millions-goodtape-auth.mdx +++ b/apps/www/_events/2024-10-23-scale-to-millions-goodtape-auth.mdx @@ -21,7 +21,7 @@ main_cta: { url: 'https://zoom.us/webinar/register/WN_26XTfO35SAC81jjS1ulWJw?amp_device_id=1bd3c92f-c97a-45ad-8aab-a508f950b536', target: '_blank', - label: 'Register', + label: 'Watch now', } speakers: 'stojan,kevinbrolly,craig_cannon,jstoone' --- diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 440d71b7035..64951f276a5 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -6,11 +6,8 @@ import Telemetry, { TelemetryEvent } from '~/lib/telemetry' import { Button } from 'ui' import SectionContainer from '~/components/Layouts/SectionContainer' -import EventCallout from '~/components/EventCallout' -import { useBreakpoint } from 'common' const Hero = () => { - const isMobile = useBreakpoint(768) const router = useRouter() const telemetryProps = useTelemetryProps() const sendTelemetryEvent = async (event: TelemetryEvent) => { @@ -25,7 +22,6 @@ const Hero = () => {

      -

      Build in a weekend Scale to millions diff --git a/apps/www/components/Nav/ProductDropdown.tsx b/apps/www/components/Nav/ProductDropdown.tsx index ecaca7ca976..7ab15b4b319 100644 --- a/apps/www/components/Nav/ProductDropdown.tsx +++ b/apps/www/components/Nav/ProductDropdown.tsx @@ -88,11 +88,6 @@ const ProductDropdown = () => { ))}

    -
    - - - -
    diff --git a/apps/www/pages/customers.tsx b/apps/www/pages/customers.tsx index 2055c0f93e3..7722b58fc92 100644 --- a/apps/www/pages/customers.tsx +++ b/apps/www/pages/customers.tsx @@ -13,8 +13,6 @@ import { motion } from 'framer-motion' import styles from '~/styles/customers.module.css' import Link from 'next/link' import { GlassPanel } from 'ui-patterns/GlassPanel' -import EventCallout from '../components/EventCallout' -import { useBreakpoint } from 'common' export async function getStaticProps() { const allPostsData = getSortedPosts({ directory: '_customers' }) @@ -33,7 +31,6 @@ export async function getStaticProps() { function CustomerStoriesPage(props: any) { const { basePath } = useRouter() - const isMobile = useBreakpoint(768) const meta = { title: 'Customer Stories | Supabase', @@ -88,7 +85,6 @@ function CustomerStoriesPage(props: any) { initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0, transition: { duration: 0.5, easing: 'easeOut' } }} > -

    Customer stories

    Discover case studies on how Supabase is being used around the world to quickly diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index 4002a65f78c..5a1018d52f3 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -9,7 +9,7 @@ https://supabase.com/customers/humata - Humata Scales with Supabase - Achieving 4X Cost Savings and Enhanced Performance + Humata Scales with Supabase: Achieving 4X Cost Savings and Enhanced Performance https://supabase.com/customers/humata By partnering with Supabase, Humata achieved a 4X reduction in vector database costs, streamlined their development, and consolidated their data so they could scale seamlessly. Mon, 30 Sep 2024 22:00:00 GMT From d8c1d1a2db4e76c8d0387635e89f45e434e18f87 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 24 Oct 2024 11:01:29 +0800 Subject: [PATCH 35/80] Chore/minor fix scratch pad (#30055) * Fix padding * Only activate scratch pad if feature preview is enabled --- .../components/ui/AIAssistantPanel/AIAssistantPanel.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx index c2d0379154a..346f7a4793e 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx @@ -40,6 +40,7 @@ import CodeEditor from '../CodeEditor/CodeEditor' import { AIAssistant } from './AIAssistant' import { generateCTA, generatePlaceholder, generateTitle, validateQuery } from './AIAssistant.utils' import { ASSISTANT_SUPPORT_ENTITIES } from './AiAssistant.constants' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' export const AiAssistantPanel = () => { const os = detectOS() @@ -48,6 +49,7 @@ export const AiAssistantPanel = () => { const project = useSelectedProject() const queryClient = useQueryClient() const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot() + const isEnabled = useIsDatabaseFunctionsAssistantEnabled() const { open, editor, content } = aiAssistantPanel const previousEditor = usePrevious(editor) @@ -249,10 +251,10 @@ export const AiAssistantPanel = () => { setAiAssistantPanel({ open: true, editor: null }) } } - if (project !== undefined) window.addEventListener('keydown', handler) + if (project !== undefined && isEnabled) window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [project]) + }, [project, isEnabled]) // [Joshen] Whenever the deps change recalculate the height of the editor useEffect(() => { @@ -293,7 +295,7 @@ export const AiAssistantPanel = () => { + )} {data.columns.map((column) => ( @@ -63,8 +76,6 @@ const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps, items: [], }, - { - name: 'Migrations', - key: 'migrations', - url: `/project/${ref}/database/migrations`, - items: [], - }, ...(!!pgNetExtensionExists ? [ { diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index d9a2048be60..c77dbacf424 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -9,7 +9,6 @@ export * from './ExpandableVideo' export * from './GlassPanel' export * from './IconPanel' export * from './PrivacySettings' -export * from './SchemaTableNode' export * from './ThemeToggle' export * from './TweetCard' export * from './InnerSideMenu' From e1c871322135f24b9492383c69133fea1a0b2cab Mon Sep 17 00:00:00 2001 From: divit <27228526+delgado3d@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:43:47 +0530 Subject: [PATCH 37/80] fix: remove incorrect usage of restoring state for restarts (#29883) --- .../Infrastructure/InfrastructureConfiguration/InstanceNode.tsx | 2 -- .../Infrastructure/InfrastructureConfiguration/MapView.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index ae014b55630..568166cc00d 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -266,8 +266,6 @@ export const ReplicaNode = ({ data }: NodeProps) => { ) : status === REPLICA_STATUS.GOING_DOWN ? ( Going down - ) : status === REPLICA_STATUS.RESTORING ? ( - Restarting ) : status === REPLICA_STATUS.RESTARTING ? ( Restarting ) : status === REPLICA_STATUS.RESIZING ? ( diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx index ca3285d6665..a7ddcaaa19b 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx @@ -271,8 +271,6 @@ const MapView = ({ Healthy ) : database.status === REPLICA_STATUS.COMING_UP ? ( Coming up - ) : database.status === REPLICA_STATUS.RESTORING ? ( - Restarting ) : database.status === REPLICA_STATUS.RESTARTING ? ( Restarting ) : database.status === REPLICA_STATUS.RESIZING ? ( From ddd52215dd4f9faba814d4c0d763418fa3e763c2 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 24 Oct 2024 11:08:04 +0200 Subject: [PATCH 38/80] fix: Optimize json data when including it in the www nav (#30041) * Add a script to separate the latest blog posts from the contentlayer json into a separate json. * Use the new json data for the latest blog posts. --- .../www/components/Nav/DevelopersDropdown.tsx | 13 ++---- apps/www/components/Nav/index.tsx | 16 ++++--- apps/www/data/nav.tsx | 5 +-- apps/www/lib/helpers.tsx | 19 --------- apps/www/package.json | 2 +- apps/www/scripts/generateLatestBlogPosts.mjs | 42 +++++++++++++++++++ 6 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 apps/www/scripts/generateLatestBlogPosts.mjs diff --git a/apps/www/components/Nav/DevelopersDropdown.tsx b/apps/www/components/Nav/DevelopersDropdown.tsx index 76dcf53941b..e3cffcda2f7 100644 --- a/apps/www/components/Nav/DevelopersDropdown.tsx +++ b/apps/www/components/Nav/DevelopersDropdown.tsx @@ -1,13 +1,8 @@ -import React from 'react' -import Link from 'next/link' import { ChevronRight } from 'lucide-react' +import Link from 'next/link' import { data as DevelopersData } from 'data/Developers' -import { BlogPost } from 'contentlayer/generated' - -type Props = { - blogPosts?: BlogPost[] -} +import blogPosts from '~/.contentlayer/generated/LatestBlogPost/_index.json' assert { type: 'json' } type LinkProps = { text: string @@ -17,7 +12,7 @@ type LinkProps = { svg?: any } -const DevelopersDropdown = ({ blogPosts }: Props) => ( +const DevelopersDropdown = () => (
    {DevelopersData['navigation'].map((column) => ( @@ -55,7 +50,7 @@ const DevelopersDropdown = ({ blogPosts }: Props) => (
      - {blogPosts?.map((post: any) => ( + {blogPosts?.map((post) => (
    • { const [open, setOpen] = useState(false) const isLoggedIn = useIsLoggedIn() const isUserLoading = useIsUserLoading() - const latestBlogPosts = allBlogPosts.sort(sortDates).slice(0, 2) - const menu = getMenu(latestBlogPosts) + const menu = getMenu() const isHomePage = router.pathname === '/' const isLaunchWeekPage = router.pathname.includes('/launch-week') diff --git a/apps/www/data/nav.tsx b/apps/www/data/nav.tsx index 4ac31ec1373..c74aa12c919 100644 --- a/apps/www/data/nav.tsx +++ b/apps/www/data/nav.tsx @@ -3,9 +3,8 @@ import ProductDropdown from '~/components/Nav/ProductDropdown' import { data as DevelopersData } from 'data/Developers' import SolutionsData from 'data/Solutions' -import { BlogPost } from 'contentlayer/generated' -export const getMenu = (latestBlogPosts: BlogPost[]) => ({ +export const getMenu = () => ({ primaryNav: [ { title: 'Product', @@ -17,7 +16,7 @@ export const getMenu = (latestBlogPosts: BlogPost[]) => ({ { title: 'Developers', hasDropdown: true, - dropdown: , + dropdown: , dropdownContainerClassName: 'rounded-xl', subMenu: DevelopersData, }, diff --git a/apps/www/lib/helpers.tsx b/apps/www/lib/helpers.tsx index 9f928c9b11e..c94a49722f1 100644 --- a/apps/www/lib/helpers.tsx +++ b/apps/www/lib/helpers.tsx @@ -44,22 +44,3 @@ export const stripEmojis = (str: string) => ) .replace(/\s+/g, ' ') .trim() - -/** - * Fixes Safari dates sorting bug - */ -export function sortDates(a: any, b: any, direction: 'asc' | 'desc' = 'desc') { - const isAsc = direction === 'asc' - var reg = /-|:|T|\+/ //The regex on which matches the string should be split (any used delimiter) -> could also be written like /[.:T\+]/ - var parsed = [ - //an array which holds the date parts for a and b - a.date.split(reg), //Split the datestring by the regex to get an array like [Year,Month,Day] - b.date.split(reg), - ] - var dates = [ - //Create an array of dates for a and b - new Date(parsed[0][0], parsed[0][1], parsed[0][2]), //Constructs an date of the above parsed parts (Year,Month... - new Date(parsed[1][0], parsed[1][1], parsed[1][2]), - ] - return isAsc ? (dates[0] as any) - (dates[1] as any) : (dates[1] as any) - (dates[0] as any) //Returns the difference between the date (if b > a then a - b < 0) -} diff --git a/apps/www/package.json b/apps/www/package.json index 48833eec172..863dea91528 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -11,7 +11,7 @@ "lint": "next lint", "clean": "rimraf node_modules", "typecheck": "npm run content:build && tsc --noEmit", - "content:build": "contentlayer2 build", + "content:build": "contentlayer2 build && node scripts/generateLatestBlogPosts.mjs", "prettier": "prettier --write \"./{pages,components,lib,stores,styles,tests}/**/*.{ts,tsx,md,js,jsx,json}\"", "postbuild": "node ./internals/generate-sitemap.mjs" }, diff --git a/apps/www/scripts/generateLatestBlogPosts.mjs b/apps/www/scripts/generateLatestBlogPosts.mjs new file mode 100644 index 00000000000..3a8a36f3cfe --- /dev/null +++ b/apps/www/scripts/generateLatestBlogPosts.mjs @@ -0,0 +1,42 @@ +import { promises as fs } from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { allBlogPosts } from '../.contentlayer/generated/index.mjs' + +/** + * Fixes Safari dates sorting bug + */ +const sortDates = (a, b, direction = 'desc') => { + const isAsc = direction === 'asc' + var reg = /-|:|T|\+/ //The regex on which matches the string should be split (any used delimiter) -> could also be written like /[.:T\+]/ + var parsed = [ + //an array which holds the date parts for a and b + a.date.split(reg), //Split the datestring by the regex to get an array like [Year,Month,Day] + b.date.split(reg), + ] + var dates = [ + //Create an array of dates for a and b + new Date(parsed[0][0], parsed[0][1], parsed[0][2]), //Constructs an date of the above parsed parts (Year,Month... + new Date(parsed[1][0], parsed[1][1], parsed[1][2]), + ] + return isAsc ? dates[0] - dates[1] : dates[1] - dates[0] //Returns the difference between the date (if b > a then a - b < 0) +} + +const latestBlogPosts = allBlogPosts + .sort(sortDates) + .slice(0, 2) + .map(({ title, url, description }) => ({ title, url, description })) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const folderPath = path.join(__dirname, '../.contentlayer/generated/LatestBlogPost') +try { + await fs.mkdir(folderPath, { recursive: true }) +} catch (error) { + if (error.code !== 'EEXIST') { + throw error + } + // Folder already exists, continue silently +} + +const filePath = path.join(__dirname, '../.contentlayer/generated/LatestBlogPost/_index.json') +await fs.writeFile(filePath, JSON.stringify(latestBlogPosts), 'utf8') From 63919938be3289c12b656c9704992c7dc59693e6 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:28:06 +0200 Subject: [PATCH 39/80] fix log drain permissions (#30071) --- apps/studio/pages/project/[ref]/settings/log-drains.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/pages/project/[ref]/settings/log-drains.tsx b/apps/studio/pages/project/[ref]/settings/log-drains.tsx index 07a350f868f..b887bae6caa 100644 --- a/apps/studio/pages/project/[ref]/settings/log-drains.tsx +++ b/apps/studio/pages/project/[ref]/settings/log-drains.tsx @@ -24,7 +24,7 @@ import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, Button } from 'ui' const LogDrainsSettings: NextPageWithLayout = () => { - const canManageLogDrains = useCheckPermissions(PermissionAction.ANALYTICS_WRITE, 'logflare') + const canManageLogDrains = useCheckPermissions(PermissionAction.ANALYTICS_ADMIN_WRITE, 'logflare') const [open, setOpen] = useState(false) const { ref } = useParams() as { ref: string } From 837b424c72f1247c67b164b28731ac1bfa888170 Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Thu, 24 Oct 2024 02:41:34 -0700 Subject: [PATCH 40/80] docs: typo fix (#30065) --- apps/docs/content/guides/platform/upgrading.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index e6f848bffa0..a4f4ff210fa 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -99,7 +99,7 @@ If you upgrade to a paid plan while your project is paused, any expired one-clic If the 90 day project restore window has expired but you need to access data contained within your project using SQL, you can attempt to restore the project into a local Postgres instance. Supabase publishes tooling that can be used for that purpose. Be aware that this workflow does not produce a complete Supabase environment with REST/Auth/Storage. Instead, it creates a standalone Postgres instance that is maximally compatible with your project's backup file to assist with recovering your data. -Downloaded backups are plain text SQL files that can be executed in Postgres to restore your project's schema and data. If you need you access data within a backup we recommend restoring into a Postgres instance built using the Supabase open source build tooling to ensure that the local instance is as similar to the original project as possible. Here are the steps to start Supabase Postgres locally and restore the backup: +Downloaded backups are plain text SQL files that can be executed in Postgres to restore your project's schema and data. If you need to access data within a backup we recommend restoring into a Postgres instance built using the Supabase open source build tooling to ensure that the local instance is as similar to the original project as possible. Here are the steps to start Supabase Postgres locally and restore the backup: Install [nix](https://nixos.org/): From 5781937739ce99d9ab2909b150ced2bfaba9f101 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 24 Oct 2024 20:59:35 +0800 Subject: [PATCH 41/80] chore: aggressive prefetching (#29987) * chore: aggressive prefetching * use abort signals in prefetchers * move encrypted columns to react-query * prefetching for filter and sort applied * prefetch remaining entity types * prefetch tables in more places * prefetch editor page on project panel * add feature flag * fix typescript * nit * Nit * fix imports * remove views check on encrypted schemas * use fetchQuery instead of prefetchQuery * fix useEncryptedColumnsQuery ts error * filter by schema on encrypted columns * don't use pg_get_tabledef for foreign tables * Remove unnecesary import --------- Co-authored-by: Joshen Lim --- apps/studio/components/grid/SupabaseGrid.tsx | 14 +- .../components/grid/SupabaseGrid.utils.ts | 62 +++++- .../formatter/ForeignKeyFormatter.tsx | 15 +- .../PolicyTableRow/PolicyTableRowHeader.tsx | 24 ++- .../interfaces/Database/Tables/TableList.tsx | 7 +- .../Database/Wrappers/WrapperRow.tsx | 9 +- .../Home/NewProjectPanel/NewProjectPanel.tsx | 8 +- .../ForeignRowSelector/ForeignRowSelector.tsx | 9 +- .../SidePanelEditor/SidePanelEditor.utils.tsx | 64 ------ .../ForeignKeysManagement/ForeignKeyRow.tsx | 11 +- .../TableGridEditor/TableGridEditor.tsx | 57 ++---- .../NavigationBar/NavigationBar.utils.tsx | 7 +- .../NavigationBar/NavigationIconLink.tsx | 27 ++- .../TableEditorLayout/EntityListItem.tsx | 6 +- .../TableEditorLayout/TableEditorMenu.tsx | 12 +- apps/studio/components/ui/ui.types.ts | 1 + .../data/database/entity-definitions-query.ts | 8 +- .../database/foreign-key-constraints-query.ts | 58 ++++-- apps/studio/data/database/schemas-query.ts | 24 ++- .../encrypted-columns-query.ts | 69 +++++++ apps/studio/data/encrypted-columns/keys.ts | 7 + .../data/entity-types/entity-type-query.ts | 30 ++- .../entity-types-infinite-query.ts | 38 +++- .../foreign-tables/foreign-table-query.ts | 11 +- .../materialized-view-query.ts | 12 +- .../prefetchers/project.$ref.editor.$id.tsx | 182 ++++++++++++++++++ .../data/prefetchers/project.$ref.editor.tsx | 64 ++++++ .../data/table-rows/table-rows-query.ts | 59 ++++-- apps/studio/data/tables/table-query.ts | 27 +-- apps/studio/data/views/view-query.ts | 11 +- apps/studio/hooks/misc/useTable.ts | 16 +- .../pages/project/[ref]/editor/[id].tsx | 2 +- apps/studio/state/table-editor.tsx | 4 +- 33 files changed, 717 insertions(+), 238 deletions(-) create mode 100644 apps/studio/data/encrypted-columns/encrypted-columns-query.ts create mode 100644 apps/studio/data/encrypted-columns/keys.ts create mode 100644 apps/studio/data/prefetchers/project.$ref.editor.$id.tsx create mode 100644 apps/studio/data/prefetchers/project.$ref.editor.tsx diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index c25b503426d..4d9472a26b9 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -28,8 +28,12 @@ import { InitialStateType } from './store/reducers' import type { SupabaseGridProps } from './types' import { getGridColumns } from './utils/gridColumns' -function onLoadStorage(storageRef: string, tableName: string, schema?: string | null) { - const storageKey = getStorageKey(STORAGE_KEY_PREFIX, storageRef) +export function loadTableEditorSortsAndFiltersFromLocalStorage( + projectRef: string, + tableName: string, + schema?: string | null +) { + const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef) const jsonStr = localStorage.getItem(storageKey) if (!jsonStr) return const json = JSON.parse(jsonStr) @@ -45,7 +49,11 @@ async function initTable( filter?: string[] // Comes directly from URL param ): Promise<{ savedState: { sorts?: string[]; filters?: string[] } }> { const savedState = props.projectRef - ? onLoadStorage(props.projectRef, props.table.name, props.table.schema) + ? loadTableEditorSortsAndFiltersFromLocalStorage( + props.projectRef, + props.table.name, + props.table.schema + ) : undefined // Check for saved state on initial load and also, load sort and filters via URL param only if given diff --git a/apps/studio/components/grid/SupabaseGrid.utils.ts b/apps/studio/components/grid/SupabaseGrid.utils.ts index 1e6a6e8c5be..25fddc31895 100644 --- a/apps/studio/components/grid/SupabaseGrid.utils.ts +++ b/apps/studio/components/grid/SupabaseGrid.utils.ts @@ -1,12 +1,20 @@ +import type { PostgresRelationship, PostgresTable } from '@supabase/postgres-meta' import AwesomeDebouncePromise from 'awesome-debounce-promise' -import type { Filter } from 'components/grid/types' import { compact } from 'lodash' -import type { Dictionary } from 'types' + +import type { Filter } from 'components/grid/types' +import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import { + ForeignKeyConstraint, + ForeignKeyConstraintsData, +} from 'data/database/foreign-key-constraints-query' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { TableLike } from 'hooks/misc/useTable' +import type { Dictionary, SchemaView } from 'types' import { FilterOperatorOptions } from './components/header/filter/Filter.constants' import { STORAGE_KEY_PREFIX } from './constants' import { InitialStateType } from './store/reducers' import type { Sort, SupabaseGridProps, SupaColumn, SupaTable } from './types' - /** * Ensure that if editable is false, we should remove all editing actions * to prevent rare-case bugs with the UI @@ -124,6 +132,54 @@ export function parseSupaTable( } } +export function getSupaTable({ + selectedTable, + entityType, + foreignKeyMeta, + encryptedColumns, +}: { + selectedTable: TableLike + entityType?: ENTITY_TYPE + foreignKeyMeta: ForeignKeyConstraintsData + encryptedColumns: string[] +}) { + // [Joshen] We can tweak below to eventually support composite keys as the data + // returned from foreignKeyMeta should be easy to deal with, rather than pg-meta + const formattedRelationships = ( + ('relationships' in selectedTable && selectedTable.relationships) || + [] + ).map((relationship: PostgresRelationship) => { + const relationshipMeta = foreignKeyMeta.find( + (fk: ForeignKeyConstraint) => fk.id === relationship.id + ) + return { + ...relationship, + deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, + } + }) + + const isViewSelected = + entityType === ENTITY_TYPE.VIEW || entityType === ENTITY_TYPE.MATERIALIZED_VIEW + const isForeignTableSelected = entityType === ENTITY_TYPE.FOREIGN_TABLE + + return !isViewSelected && !isForeignTableSelected + ? parseSupaTable( + { + table: selectedTable as PostgresTable, + columns: (selectedTable as PostgresTable).columns ?? [], + primaryKeys: (selectedTable as PostgresTable).primary_keys ?? [], + relationships: formattedRelationships, + }, + encryptedColumns + ) + : parseSupaTable({ + table: selectedTable as SchemaView, + columns: (selectedTable as SchemaView).columns ?? [], + primaryKeys: [], + relationships: [], + }) +} + export const saveStorageDebounced = AwesomeDebouncePromise(saveStorage, 500) function saveStorage( diff --git a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx index 7738c14b7d3..2948e7ff0f5 100644 --- a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx +++ b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx @@ -1,9 +1,9 @@ import { ArrowRight } from 'lucide-react' -import Link from 'next/link' import type { PropsWithChildren } from 'react' import type { RenderCellProps } from 'react-data-grid' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { useTableQuery } from 'data/tables/table-query' import { useTablesQuery } from 'data/tables/tables-query' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' @@ -62,11 +62,20 @@ export const ForeignKeyFormatter = (props: Props) => { className="translate-y-[2px]" style={{ padding: '3px' }} > - { - const router = useRouter() - const { ref } = router.query + const { ref } = useParams() const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') @@ -42,7 +42,11 @@ const PolicyTableRowHeader = ({ return (
      - + {table.rls_enabled ? (
      @@ -53,7 +57,7 @@ const PolicyTableRowHeader = ({
      )}

      {table.name}

      - +
      {isTableLocked && ( diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index 1242353bfee..519347c7858 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -30,12 +30,12 @@ import { useDatabasePublicationsQuery } from 'data/database-publications/databas import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { useForeignTablesQuery } from 'data/foreign-tables/foreign-tables-query' import { useMaterializedViewsQuery } from 'data/materialized-views/materialized-views-query' +import { usePrefetchEditorTablePage } from 'data/prefetchers/project.$ref.editor.$id' import { useTablesQuery } from 'data/tables/tables-query' import { useViewsQuery } from 'data/views/views-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' -import { useTableEditorStateSnapshot } from 'state/table-editor' import { Button, Checkbox_Shadcn_, @@ -74,6 +74,8 @@ const TableList = ({ const { ref } = useParams() const { project } = useProjectContext() + const prefetchEditorTablePage = usePrefetchEditorTablePage() + const { selectedSchema, setSelectedSchema } = useQuerySchemaState() const [filterString, setFilterString] = useState('') @@ -476,6 +478,9 @@ const TableList = ({ onClick={() => router.push(`/project/${project?.ref}/editor/${x.id}`) } + onMouseEnter={() => + prefetchEditorTablePage({ id: x.id ? String(x.id) : undefined }) + } >

      View in Table Editor

      diff --git a/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index ffdb8ae54d9..6dc505446c8 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -7,6 +7,7 @@ import Link from 'next/link' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { FDW } from 'data/fdw/fdws-query' +import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Collapsible } from 'ui' import type { WrapperMeta } from './Wrappers.types' @@ -119,11 +120,15 @@ const WrapperRow = ({
      {wrapper.tables ? ( wrapper.tables.map((table: any) => ( - +
      {table.name}
      - +
      )) ) : (

      No tables available

      diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index 19b5708425e..e77fbe11591 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -1,8 +1,9 @@ import { ExternalLink } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' +import { useParams } from 'common' import Panel from 'components/ui/Panel' +import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons' import { Button } from 'ui' @@ -10,8 +11,7 @@ import APIKeys from './APIKeys' import GetStartedHero from './GetStartedHero' const NewProjectPanel = () => { - const router = useRouter() - const { ref } = router.query + const { ref } = useParams() const { projectAuthAll: authEnabled, @@ -47,7 +47,7 @@ const NewProjectPanel = () => {
      diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 431ee1b7d5b..693f25789e5 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -1,4 +1,4 @@ -import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta' +import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { QueryKey, useQueryClient } from '@tanstack/react-query' import { find, isUndefined } from 'lodash' @@ -8,14 +8,11 @@ import { toast } from 'sonner' import { useParams } from 'common' import { SupabaseGrid } from 'components/grid/SupabaseGrid' -import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' +import { getSupaTable } from 'components/grid/SupabaseGrid.utils' import { SupaTable } from 'components/grid/types' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' -import { - ForeignKeyConstraint, - useForeignKeyConstraintsQuery, -} from 'data/database/foreign-key-constraints-query' +import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' +import { useEncryptedColumnsQuery } from 'data/encrypted-columns/encrypted-columns-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { sqlKeys } from 'data/sql/keys' import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation' @@ -28,13 +25,12 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { EMPTY_ARR } from 'lib/void' import { useGetImpersonatedRole } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' -import type { Dictionary, SchemaView } from 'types' +import type { Dictionary } from 'types' import { Button } from 'ui' import GridHeaderActions from './GridHeaderActions' import { TableGridSkeletonLoader } from './LoadingState' import NotFoundState from './NotFoundState' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' -import { useEncryptedColumns } from './SidePanelEditor/SidePanelEditor.utils' import TableDefinition from './TableDefinition' export interface TableGridEditorProps { @@ -62,8 +58,10 @@ const TableGridEditor = ({ const canEditColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') const isReadOnly = !canEditTables && !canEditColumns - const encryptedColumns = useEncryptedColumns({ - schemaName: selectedTable?.schema, + const { data: encryptedColumns } = useEncryptedColumnsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: selectedTable?.schema, tableName: selectedTable?.name, }) @@ -148,43 +146,16 @@ const TableGridEditor = ({ const isViewSelected = entityType?.type === ENTITY_TYPE.VIEW || entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW const isTableSelected = entityType?.type === ENTITY_TYPE.TABLE - const isForeignTableSelected = entityType?.type === ENTITY_TYPE.FOREIGN_TABLE const isLocked = EXCLUDED_SCHEMAS.includes(entityType?.schema ?? '') const canEditViaTableEditor = isTableSelected && !isLocked - // [Joshen] We can tweak below to eventually support composite keys as the data - // returned from foreignKeyMeta should be easy to deal with, rather than pg-meta - const formattedRelationships = ( - ('relationships' in selectedTable && selectedTable.relationships) || - [] - ).map((relationship: PostgresRelationship) => { - const relationshipMeta = foreignKeyMeta.find( - (fk: ForeignKeyConstraint) => fk.id === relationship.id - ) - return { - ...relationship, - deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, - } + const gridTable = getSupaTable({ + encryptedColumns, + foreignKeyMeta, + selectedTable, + entityType: entityType?.type, }) - const gridTable = - !isViewSelected && !isForeignTableSelected - ? parseSupaTable( - { - table: selectedTable as PostgresTable, - columns: (selectedTable as PostgresTable).columns ?? [], - primaryKeys: (selectedTable as PostgresTable).primary_keys ?? [], - relationships: formattedRelationships, - }, - encryptedColumns - ) - : parseSupaTable({ - table: selectedTable as SchemaView, - columns: (selectedTable as SchemaView).columns ?? [], - primaryKeys: [], - relationships: [], - }) - const gridKey = `${selectedTable.schema}_${selectedTable.name}` const onTableCreated = (table: PostgresTable) => { diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 159f5bb7dae..8b807064bc6 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -1,6 +1,8 @@ +import { Blocks, FileText, Lightbulb, List, Settings } from 'lucide-react' + import type { Route } from 'components/ui/ui.types' +import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' import type { Project } from 'data/projects/project-detail-query' -import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { Auth, Database, @@ -11,8 +13,8 @@ import { Storage, TableEditor, } from 'icons' +import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar' -import { Settings, FileText, List, Lightbulb, Blocks } from 'lucide-react' export const generateToolRoutes = (ref?: string, project?: Project): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP @@ -24,6 +26,7 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] => label: 'Table Editor', icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`), + linkComponent: , }, { key: 'sql', diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx index ca8b6143169..758deedc315 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx @@ -1,13 +1,18 @@ import { noop } from 'lodash' import Link from 'next/link' -import { AnchorHTMLAttributes, forwardRef } from 'react' -import { cn } from 'ui' -import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { + AnchorHTMLAttributes, + cloneElement, + ComponentPropsWithoutRef, + forwardRef, + isValidElement, +} from 'react' +import { cn, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' import type { Route } from 'components/ui/ui.types' -import { useAppStateSnapshot } from 'state/app-state' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { LOCAL_STORAGE_KEYS } from 'lib/constants' +import { useAppStateSnapshot } from 'state/app-state' interface NavigationIconButtonProps extends AnchorHTMLAttributes { route: Route @@ -41,8 +46,18 @@ const NavigationIconLink = forwardRef>( + function LinkComponent(props, ref) { + if (route.linkComponent && isValidElement(route.linkComponent)) { + return cloneElement(route.linkComponent, { ...props, ref }) + } + + return + } + ) + const linkContent = ( - {route.label} - + ) if (!allowNavPanelToExpand) { diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index 2a2f463ea13..98cd65ec2b4 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -46,6 +46,7 @@ import { } from 'ui' import { useProjectContext } from '../ProjectLayout/ProjectContext' import { Markdown } from 'components/interfaces/Markdown' +import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' export interface EntityListItemProps { id: number @@ -277,8 +278,9 @@ const EntityListItem: ItemRenderer = ({ } return ( - = ({ )} - + ) } diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index c859e571e41..82ff2ae0376 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -12,10 +12,10 @@ import SchemaSelector from 'components/ui/SchemaSelector' import { useSchemasQuery } from 'data/database/schemas-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' -import { useTableQuery } from 'data/tables/table-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' +import useTable from 'hooks/misc/useTable' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { useTableEditorStateSnapshot } from 'state/table-editor' import { @@ -96,15 +96,7 @@ const TableEditorMenu = () => { ) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) - const { data: selectedTable } = useTableQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - id: Number(id), - }, - // only run if we have a selected table - { enabled: Boolean(id) } - ) + const { data: selectedTable } = useTable(id !== undefined ? Number(id) : undefined) useEffect(() => { if (selectedTable) { diff --git a/apps/studio/components/ui/ui.types.ts b/apps/studio/components/ui/ui.types.ts index 8ede34b7f18..2aee180817c 100644 --- a/apps/studio/components/ui/ui.types.ts +++ b/apps/studio/components/ui/ui.types.ts @@ -6,4 +6,5 @@ export interface Route { icon: ReactNode link?: string disabled?: boolean + linkComponent?: ReactNode } diff --git a/apps/studio/data/database/entity-definitions-query.ts b/apps/studio/data/database/entity-definitions-query.ts index e288f81fa0e..4506ea58ec9 100644 --- a/apps/studio/data/database/entity-definitions-query.ts +++ b/apps/studio/data/database/entity-definitions-query.ts @@ -32,13 +32,7 @@ with records as ( 'create materialized view ', concat(nc.nspname, '.', c.relname), ' as', pg_get_viewdef(concat(nc.nspname, '.', c.relname), true) ) - when 'f' then pg_temp.pg_get_tabledef( - concat(nc.nspname), - concat(c.relname), - false, - 'FKEYS_INTERNAL', - 'NO_TRIGGERS' - ) + when 'f' then concat('create foreign table ', nc.nspname, '.', c.relname, ' ( ... )') when 'p' then pg_temp.pg_get_tabledef( concat(nc.nspname), concat(c.relname), diff --git a/apps/studio/data/database/foreign-key-constraints-query.ts b/apps/studio/data/database/foreign-key-constraints-query.ts index 3c348b11ec2..7c34c7894fd 100644 --- a/apps/studio/data/database/foreign-key-constraints-query.ts +++ b/apps/studio/data/database/foreign-key-constraints-query.ts @@ -1,5 +1,12 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { QueryClient, UseQueryOptions } from '@tanstack/react-query' + +import { sqlKeys } from 'data/sql/keys' +import { + executeSql, + ExecuteSqlData, + ExecuteSqlError, + useExecuteSqlQuery, +} from '../sql/execute-sql-query' type GetForeignKeyConstraintsVariables = { schema?: string @@ -96,6 +103,22 @@ export type ForeignKeyConstraintsVariables = GetForeignKeyConstraintsVariables & export type ForeignKeyConstraintsData = ForeignKeyConstraint[] export type ForeignKeyConstraintsError = ExecuteSqlError +function select( + data: + | { + result: any + } + | undefined +) { + return (data?.result ?? []).map((foreignKey: ForeignKeyConstraintRaw) => { + return { + ...foreignKey, + source_columns: foreignKey.source_columns.replace('{', '').replace('}', '').split(','), + target_columns: foreignKey.target_columns.replace('{', '').replace('}', '').split(','), + } + }) +} + export const useForeignKeyConstraintsQuery = < TData extends ForeignKeyConstraintsData = ForeignKeyConstraintsData, >( @@ -107,19 +130,30 @@ export const useForeignKeyConstraintsQuery = < projectRef, connectionString, sql: getForeignKeyConstraintsQuery({ schema }), - queryKey: ['foreign-key-constraints'], + queryKey: ['foreign-key-constraints', schema], }, { - select(data) { - return ((data as any)?.result ?? []).map((foreignKey: ForeignKeyConstraintRaw) => { - return { - ...foreignKey, - source_columns: foreignKey.source_columns.replace('{', '').replace('}', '').split(','), - target_columns: foreignKey.target_columns.replace('{', '').replace('}', '').split(','), - } - }) - }, + select, ...options, } ) } + +export function prefetchForeignKeyConstraints( + client: QueryClient, + { projectRef, connectionString, schema }: ForeignKeyConstraintsVariables +) { + return client + .fetchQuery(sqlKeys.query(projectRef, ['foreign-key-constraints', schema]), ({ signal }) => + executeSql( + { + projectRef, + connectionString, + sql: getForeignKeyConstraintsQuery({ schema }), + queryKey: ['foreign-key-constraints', schema], + }, + signal + ) + ) + .then((data) => select(data) as ForeignKeyConstraintsData) +} diff --git a/apps/studio/data/database/schemas-query.ts b/apps/studio/data/database/schemas-query.ts index 911f8fc88fa..22e27c76092 100644 --- a/apps/studio/data/database/schemas-query.ts +++ b/apps/studio/data/database/schemas-query.ts @@ -2,7 +2,12 @@ import pgMeta from '@supabase/pg-meta' import { QueryClient, UseQueryOptions } from '@tanstack/react-query' import { z } from 'zod' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from 'data/sql/execute-sql-query' +import { + executeSql, + ExecuteSqlData, + ExecuteSqlError, + useExecuteSqlQuery, +} from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' export type SchemasVariables = { @@ -39,3 +44,20 @@ export const useSchemasQuery = ( export function invalidateSchemasQuery(client: QueryClient, projectRef: string | undefined) { return client.invalidateQueries(sqlKeys.query(projectRef, ['schemas', 'list'])) } + +export function prefetchSchemas( + client: QueryClient, + { projectRef, connectionString }: SchemasVariables +) { + return client.fetchQuery(sqlKeys.query(projectRef, ['schemas', 'list']), ({ signal }) => + executeSql( + { + projectRef, + connectionString, + sql: pgMetaSchemasList.sql, + queryKey: ['schemas', 'list'], + }, + signal + ) + ) +} diff --git a/apps/studio/data/encrypted-columns/encrypted-columns-query.ts b/apps/studio/data/encrypted-columns/encrypted-columns-query.ts new file mode 100644 index 00000000000..e21e2685631 --- /dev/null +++ b/apps/studio/data/encrypted-columns/encrypted-columns-query.ts @@ -0,0 +1,69 @@ +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { encryptedColumnKeys } from './keys' + +export type EncryptedColumnsVariables = { + projectRef?: string + connectionString?: string + schema?: string + tableName?: string +} + +export async function getEncryptedColumns( + { projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables, + signal?: AbortSignal +) { + if (!tableName) return [] + + try { + const encryptedColumns = await executeSql( + { + projectRef, + connectionString, + sql: `select column_name as name from information_schema.columns where table_schema = '${schema}' and table_name = 'decrypted_${tableName}' and column_name like 'decrypted_%'`, + queryKey: ['encrypted-columns', tableName], + }, + signal + ) + return encryptedColumns.result.map((column: any) => column.name.split('decrypted_')[1]) + } catch (error) { + console.error('Error fetching encrypted columns', error) + return [] + } +} + +export type EncryptedColumnsData = Awaited> +export type EncryptedColumnsError = ResponseError + +export const useEncryptedColumnsQuery = ( + { projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + encryptedColumnKeys.list(projectRef, schema, tableName), + ({ signal }) => + getEncryptedColumns({ projectRef, connectionString, schema, tableName }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) + +export function prefetchEncryptedColumns( + client: QueryClient, + { projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables +) { + return client.fetchQuery(encryptedColumnKeys.list(projectRef, schema, tableName), ({ signal }) => + getEncryptedColumns( + { + projectRef, + connectionString, + schema, + tableName, + }, + signal + ) + ) +} diff --git a/apps/studio/data/encrypted-columns/keys.ts b/apps/studio/data/encrypted-columns/keys.ts new file mode 100644 index 00000000000..e73d19d7a53 --- /dev/null +++ b/apps/studio/data/encrypted-columns/keys.ts @@ -0,0 +1,7 @@ +export const encryptedColumnKeys = { + list: ( + projectRef: string | undefined, + schema: string | undefined, + tableName: string | undefined + ) => ['projects', projectRef, 'encrypted-columns', schema, tableName] as const, +} diff --git a/apps/studio/data/entity-types/entity-type-query.ts b/apps/studio/data/entity-types/entity-type-query.ts index c650159ce52..b0e26577073 100644 --- a/apps/studio/data/entity-types/entity-type-query.ts +++ b/apps/studio/data/entity-types/entity-type-query.ts @@ -1,5 +1,12 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { QueryClient, UseQueryOptions } from '@tanstack/react-query' + +import { sqlKeys } from 'data/sql/keys' +import { + executeSql, + ExecuteSqlData, + ExecuteSqlError, + useExecuteSqlQuery, +} from '../sql/execute-sql-query' import { ENTITY_TYPE } from './entity-type-constants' type EntityTypeArgs = { @@ -75,3 +82,22 @@ export const useEntityTypeQuery = + executeSql( + { + projectRef, + connectionString, + sql: entityTypeSqlQuery({ id }), + queryKey: ['entity-type', id], + }, + signal + ) + ) + .then((data: { result: EntityTypeData[] }) => data?.result?.[0] ?? null) +} diff --git a/apps/studio/data/entity-types/entity-types-infinite-query.ts b/apps/studio/data/entity-types/entity-types-infinite-query.ts index 3931fb892e9..91d1b549bd9 100644 --- a/apps/studio/data/entity-types/entity-types-infinite-query.ts +++ b/apps/studio/data/entity-types/entity-types-infinite-query.ts @@ -1,7 +1,8 @@ -import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query' +import { QueryClient, useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query' import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query' import type { Entity } from './entity-type-query' import { entityTypeKeys } from './keys' +import { ENTITY_TYPE } from './entity-type-constants' export type EntityTypesVariables = { projectRef?: string @@ -29,7 +30,7 @@ export async function getEntityTypes( limit = 100, page = 0, sort = 'alphabetical', - filterTypes, + filterTypes = Object.values(ENTITY_TYPE), }: EntityTypesVariables, signal?: AbortSignal ) { @@ -57,7 +58,7 @@ export async function getEntityTypes( pg_namespace nc join pg_class c on nc.oid = c.relnamespace where - c.relkind in (${filterTypes === undefined ? `'r', 'v', 'm', 'f', 'p'` : filterTypes.map((x) => `'${x}'`).join(', ')}) + c.relkind in (${filterTypes.map((x) => `'${x}'`).join(', ')}) and not pg_is_other_temp_schema(nc.oid) and ( pg_has_role(c.relowner, 'USAGE') @@ -155,3 +156,34 @@ export const useEntityTypesQuery = ( ...options, } ) + +export function prefetchEntityTypes( + client: QueryClient, + { + projectRef, + connectionString, + schemas = ['public'], + search, + limit = 100, + sort, + filterTypes, + }: Omit +) { + return client.prefetchInfiniteQuery( + entityTypeKeys.list(projectRef, { schemas, search, sort, limit, filterTypes }), + ({ signal, pageParam }) => + getEntityTypes( + { + projectRef, + connectionString, + schemas, + search, + limit, + page: pageParam, + sort, + filterTypes, + }, + signal + ) + ) +} diff --git a/apps/studio/data/foreign-tables/foreign-table-query.ts b/apps/studio/data/foreign-tables/foreign-table-query.ts index d6e3af3748f..cc7370d4ff8 100644 --- a/apps/studio/data/foreign-tables/foreign-table-query.ts +++ b/apps/studio/data/foreign-tables/foreign-table-query.ts @@ -1,5 +1,5 @@ import type { PostgresTable } from '@supabase/postgres-meta' -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { foreignTableKeys } from './keys' @@ -56,3 +56,12 @@ export const useForeignTableQuery = ( ...options, } ) + +export function prefetchForeignTable( + client: QueryClient, + { projectRef, connectionString, id }: ForeignTableVariables +) { + return client.fetchQuery(foreignTableKeys.foreignTable(projectRef, id), ({ signal }) => + getForeignTable({ id, projectRef, connectionString }, signal) + ) +} diff --git a/apps/studio/data/materialized-views/materialized-view-query.ts b/apps/studio/data/materialized-views/materialized-view-query.ts index 0b714667b9b..1d6e2411e24 100644 --- a/apps/studio/data/materialized-views/materialized-view-query.ts +++ b/apps/studio/data/materialized-views/materialized-view-query.ts @@ -1,5 +1,6 @@ import type { PostgresMaterializedView } from '@supabase/postgres-meta' -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' + import { get } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { materializedViewKeys } from './keys' @@ -59,3 +60,12 @@ export const useMaterializedViewQuery = ( ...options, } ) + +export function prefetchMaterializedView( + client: QueryClient, + { projectRef, connectionString, id }: MaterializedViewVariables +) { + return client.fetchQuery(materializedViewKeys.materializedView(projectRef, id), ({ signal }) => + getMaterializedView({ id, projectRef, connectionString }, signal) + ) +} diff --git a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx new file mode 100644 index 00000000000..ad4ed80b1fe --- /dev/null +++ b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx @@ -0,0 +1,182 @@ +import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { ComponentProps, PropsWithChildren, useCallback } from 'react' + +import { loadTableEditorSortsAndFiltersFromLocalStorage } from 'components/grid/SupabaseGrid' +import { + formatFilterURLParams, + formatSortURLParams, + getSupaTable, +} from 'components/grid/SupabaseGrid.utils' +import { Filter, Sort } from 'components/grid/types' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { + ForeignKeyConstraintsData, + prefetchForeignKeyConstraints, +} from 'data/database/foreign-key-constraints-query' +import { prefetchEncryptedColumns } from 'data/encrypted-columns/encrypted-columns-query' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { Entity, prefetchEntityType } from 'data/entity-types/entity-type-query' +import { ForeignTableData, prefetchForeignTable } from 'data/foreign-tables/foreign-table-query' +import { + MaterializedViewData, + prefetchMaterializedView, +} from 'data/materialized-views/materialized-view-query' +import { prefetchTableRows } from 'data/table-rows/table-rows-query' +import { prefetchTable, TableData } from 'data/tables/table-query' +import { prefetchView, ViewData } from 'data/views/view-query' +import { useFlag } from 'hooks/ui/useFlag' +import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' +import { TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE } from 'state/table-editor' + +export function usePrefetchEditorTablePage() { + const router = useRouter() + const queryClient = useQueryClient() + const { project } = useProjectContext() + const roleImpersonationState = useRoleImpersonationStateSnapshot() + const tableEditorPrefetchingEnabled = useFlag('tableEditorPrefetching') + + return useCallback( + ({ id: _id, filters, sorts }: { id?: string; filters?: Filter[]; sorts?: Sort[] }) => { + if (!tableEditorPrefetchingEnabled) return + + const id = _id ? Number(_id) : undefined + if (!project || !id || isNaN(id)) return + + // Prefetch the code + router.prefetch(`/project/${project.ref}/editor/${id}`) + + // Prefetch the data + prefetchEntityType(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + id, + }) + .then((entity) => { + let promises: [ + Promise, + Promise, + Promise, + ( + | Promise + | undefined + ), + ] = [ + Promise.resolve(entity), + prefetchForeignKeyConstraints(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + schema: entity?.schema, + }), + prefetchEncryptedColumns(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + schema: entity?.schema, + tableName: entity?.name, + }), + undefined, + ] + + switch (entity?.type) { + case ENTITY_TYPE.TABLE: + case ENTITY_TYPE.PARTITIONED_TABLE: + promises[3] = prefetchTable(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + id, + }) + break + + case ENTITY_TYPE.VIEW: + promises[3] = prefetchView(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + id, + }) + break + + case ENTITY_TYPE.MATERIALIZED_VIEW: + promises[3] = prefetchMaterializedView(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + id, + }) + break + + case ENTITY_TYPE.FOREIGN_TABLE: + promises[3] = prefetchForeignTable(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + id, + }) + break + } + + return Promise.all(promises) + }) + .then(([entity, foreignKeyConstraints, encryptedColumns, table]) => { + if (entity && table) { + const supaTable = getSupaTable({ + selectedTable: table, + encryptedColumns, + foreignKeyMeta: foreignKeyConstraints, + entityType: entity.type, + }) + + const { sorts: localSorts = [], filters: localFilters = [] } = + loadTableEditorSortsAndFiltersFromLocalStorage( + project.ref, + entity.name, + entity.schema + ) ?? {} + + prefetchTableRows(queryClient, { + queryKey: [supaTable.schema, supaTable.name], + projectRef: project?.ref, + connectionString: project?.connectionString, + table: supaTable, + sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts), + filters: filters ?? formatFilterURLParams(localFilters), + page: 1, + limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, + impersonatedRole: roleImpersonationState.role, + }) + } + }) + }, + [project, queryClient, router] + ) +} + +type LinkProps = ComponentProps + +interface EditorTablePageLinkProps extends Omit { + projectRef?: string + id?: string + sorts?: Sort[] + filters?: Filter[] + href?: LinkProps['href'] +} + +export function EditorTablePageLink({ + projectRef, + id, + sorts, + filters, + href, + children, + ...props +}: PropsWithChildren) { + const prefetch = usePrefetchEditorTablePage() + + return ( + prefetch({ id, sorts, filters })} + {...props} + > + {children} + + ) +} diff --git a/apps/studio/data/prefetchers/project.$ref.editor.tsx b/apps/studio/data/prefetchers/project.$ref.editor.tsx new file mode 100644 index 00000000000..e21d6fed797 --- /dev/null +++ b/apps/studio/data/prefetchers/project.$ref.editor.tsx @@ -0,0 +1,64 @@ +import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' +import { ComponentProps, PropsWithChildren, useCallback } from 'react' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { prefetchSchemas } from 'data/database/schemas-query' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { prefetchEntityTypes } from 'data/entity-types/entity-types-infinite-query' +import { useLocalStorage } from 'hooks/misc/useLocalStorage' +import { useFlag } from 'hooks/ui/useFlag' +import { useRouter } from 'next/router' + +export function usePrefetchEditorIndexPage() { + const router = useRouter() + const queryClient = useQueryClient() + const { project } = useProjectContext() + const tableEditorPrefetchingEnabled = useFlag('tableEditorPrefetching') + + const [entityTypesSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>( + 'table-editor-sort', + 'alphabetical' + ) + + return useCallback(() => { + if (!tableEditorPrefetchingEnabled || !project) return + + // Prefetch code + router.prefetch(`/project/${project.ref}/editor`) + + // Prefetch data + prefetchSchemas(queryClient, { + projectRef: project.ref, + connectionString: project.connectionString, + }) + prefetchEntityTypes(queryClient, { + projectRef: project?.ref, + connectionString: project?.connectionString, + sort: entityTypesSort, + filterTypes: Object.values(ENTITY_TYPE), + }) + }, [entityTypesSort, project, queryClient, router]) +} + +type LinkProps = ComponentProps + +interface EditorIndexPageLinkProps extends Omit { + projectRef?: string + href?: LinkProps['href'] +} + +export function EditorIndexPageLink({ + href, + projectRef, + children, + ...props +}: PropsWithChildren) { + const prefetch = usePrefetchEditorIndexPage() + + return ( + + {children} + + ) +} diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index dfa8e2b3dc4..39ecb7dce9a 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -1,4 +1,4 @@ -import type { QueryKey, UseQueryOptions } from '@tanstack/react-query' +import type { QueryClient, QueryKey, UseQueryOptions } from '@tanstack/react-query' import { IS_PLATFORM } from 'common' import { Query } from 'components/grid/query/Query' @@ -7,13 +7,17 @@ import { JSON_TYPES, TEXT_TYPES, } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants' +import { sqlKeys } from 'data/sql/keys' import { KB } from 'lib/constants' import { ImpersonationRole, ROLE_IMPERSONATION_NO_RESULTS, wrapWithRoleImpersonation, } from 'lib/role-impersonation' -import { useIsRoleImpersonationEnabled } from 'state/role-impersonation-state' +import { + isRoleImpersonationEnabled, + useIsRoleImpersonationEnabled, +} from 'state/role-impersonation-state' import { ExecuteSqlData, ExecuteSqlError, @@ -204,6 +208,21 @@ export type TableRowsVariables = GetTableRowsArgs & { export type TableRowsData = TableRows export type TableRowsError = ExecuteSqlError +function getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }: TableRowsVariables) { + return [ + ...(queryKey ?? []), + { + table: { + name: table?.name, + schema: table?.schema, + columns: table?.columns.map((c) => c.name), + }, + impersonatedRole, + ...args, + }, + ] +} + export const useTableRowsQuery = ( { projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables, options: UseQueryOptions = {} @@ -218,18 +237,7 @@ export const useTableRowsQuery = ( projectRef: projectRef ?? 'ref', role: impersonatedRole, }), - queryKey: [ - ...(queryKey ?? []), - { - table: { - name: table?.name, - schema: table?.schema, - columns: table?.columns.map((c) => c.name), - }, - impersonatedRole, - ...args, - }, - ], + queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }), isRoleImpersonationEnabled, }, { @@ -247,3 +255,26 @@ export const useTableRowsQuery = ( } ) } + +export function prefetchTableRows( + client: QueryClient, + { projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables +) { + return client.fetchQuery( + sqlKeys.query(projectRef, getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args })), + ({ signal }) => + executeSql( + { + projectRef, + connectionString, + sql: wrapWithRoleImpersonation(getTableRowsSqlQuery({ table, ...args }), { + projectRef: projectRef ?? 'ref', + role: impersonatedRole, + }), + queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }), + isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole), + }, + signal + ) + ) +} diff --git a/apps/studio/data/tables/table-query.ts b/apps/studio/data/tables/table-query.ts index 2e8bea2b5dd..36b5d4195a3 100644 --- a/apps/studio/data/tables/table-query.ts +++ b/apps/studio/data/tables/table-query.ts @@ -1,6 +1,5 @@ import type { PostgresTable } from '@supabase/postgres-meta' -import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -import { useCallback } from 'react' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' import { tableKeys } from './keys' @@ -57,23 +56,11 @@ export const useTableQuery = ( } ) -/** - * useGetTable - * Tries to get a table from the react-query cache, or loads it from the server if it's not cached. - */ -export function useGetTable({ - projectRef, - connectionString, -}: Pick) { - const queryClient = useQueryClient() - - return useCallback( - (id: NonNullable) => { - return queryClient.fetchQuery({ - queryKey: tableKeys.table(projectRef, id), - queryFn: ({ signal }) => getTable({ id, projectRef, connectionString }, signal), - }) - }, - [connectionString, projectRef, queryClient] +export function prefetchTable( + client: QueryClient, + { projectRef, connectionString, id }: TableVariables +) { + return client.fetchQuery(tableKeys.table(projectRef, id), ({ signal }) => + getTable({ id, projectRef, connectionString }, signal) ) } diff --git a/apps/studio/data/views/view-query.ts b/apps/studio/data/views/view-query.ts index ae6d11b6c2d..9fc16cf2299 100644 --- a/apps/studio/data/views/view-query.ts +++ b/apps/studio/data/views/view-query.ts @@ -1,5 +1,5 @@ import type { PostgresView } from '@supabase/postgres-meta' -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' @@ -55,3 +55,12 @@ export const useViewQuery = ( ...options, } ) + +export function prefetchView( + client: QueryClient, + { projectRef, connectionString, id }: ViewVariables +) { + return client.fetchQuery(viewKeys.view(projectRef, id), ({ signal }) => + getView({ id, projectRef, connectionString }, signal) + ) +} diff --git a/apps/studio/hooks/misc/useTable.ts b/apps/studio/hooks/misc/useTable.ts index 17bf4042be1..891d25fe9df 100644 --- a/apps/studio/hooks/misc/useTable.ts +++ b/apps/studio/hooks/misc/useTable.ts @@ -25,18 +25,7 @@ function useTable(id?: number) { id: entity?.id, }, { - enabled: entity?.type === ENTITY_TYPE.TABLE, - } - ) - - const partitionTableResult = useTableQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - id: entity?.id, - }, - { - enabled: entity?.type === ENTITY_TYPE.PARTITIONED_TABLE, + enabled: entity?.type === ENTITY_TYPE.TABLE || entity?.type === ENTITY_TYPE.PARTITIONED_TABLE, } ) @@ -75,9 +64,8 @@ function useTable(id?: number) { switch (entity?.type) { case ENTITY_TYPE.TABLE: - return tableResult case ENTITY_TYPE.PARTITIONED_TABLE: - return partitionTableResult + return tableResult case ENTITY_TYPE.VIEW: return viewResult case ENTITY_TYPE.MATERIALIZED_VIEW: diff --git a/apps/studio/pages/project/[ref]/editor/[id].tsx b/apps/studio/pages/project/[ref]/editor/[id].tsx index 69240d08908..9c232bb3883 100644 --- a/apps/studio/pages/project/[ref]/editor/[id].tsx +++ b/apps/studio/pages/project/[ref]/editor/[id].tsx @@ -31,7 +31,7 @@ const TableEditorPage: NextPageWithLayout = () => { if (tables.length > 0) { router.push(`/project/${projectRef}/editor/${tables[0].id}`) } else { - router.push(`/project/${projectRef}/editor/`) + router.push(`/project/${projectRef}/editor`) } }} /> diff --git a/apps/studio/state/table-editor.tsx b/apps/studio/state/table-editor.tsx index a55245d9ba3..05367f0153f 100644 --- a/apps/studio/state/table-editor.tsx +++ b/apps/studio/state/table-editor.tsx @@ -6,6 +6,8 @@ import { PropsWithChildren, createContext, useContext, useRef } from 'react' import type { Dictionary } from 'types' import { proxy, useSnapshot } from 'valtio' +export const TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE = 100 + type ForeignKeyState = { foreignKey: ForeignKey row: Dictionary @@ -63,7 +65,7 @@ export const createTableEditorState = () => { setPage: (page: number) => { state.page = page }, - rowsPerPage: 100, + rowsPerPage: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, setRowsPerPage: (rowsPerPage: number) => { state.rowsPerPage = rowsPerPage }, From 3a3360bbd0eb7d3a6de80fe9728f21cc91ccb18d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 24 Oct 2024 21:32:47 +0800 Subject: [PATCH 42/80] Fix all usage of button tooltip (#30076) * Fix all usage of button tooltip * PRETTY --- .../components/interfaces/Auth/Hooks/HookCard.tsx | 4 +++- .../Auth/Policies/AIPolicyEditorPanel/index.tsx | 4 +++- .../Auth/Policies/PolicyTableRow/PolicyRow.tsx | 3 +-- .../PolicyTableRow/PolicyTableRowHeader.tsx | 8 ++++++-- .../Auth/RedirectUrls/RedirectUrlList.tsx | 8 ++++++-- .../interfaces/Auth/Users/UserOverview.tsx | 8 ++++++-- .../BranchManagement/BranchManagement.tsx | 8 ++++++-- .../interfaces/BranchManagement/EmptyStates.tsx | 4 +++- .../DataWarehouse/CreateWarehouseAccessToken.tsx | 4 +++- .../Functions/FunctionsList/FunctionsList.tsx | 4 +++- .../interfaces/Database/Tables/ColumnList.tsx | 4 +++- .../interfaces/Database/Tables/TableList.tsx | 4 +++- .../EdgeFunctionDetails/EdgeFunctionDetails.tsx | 4 +++- .../EdgeFunctionSecrets/EdgeFunctionSecret.tsx | 4 +++- .../EdgeFunctionSecrets/EdgeFunctionSecrets.tsx | 4 +++- .../Integrations/CronJobs/CreateCronJobSheet.tsx | 4 +++- .../interfaces/Integrations/IntegrationPanels.tsx | 5 ++++- .../Organization/OAuthApps/OAuthApps.tsx | 4 +++- .../interfaces/Settings/API/HardenAPIModal.tsx | 15 +++++++++++++-- .../interfaces/Settings/Database/BannedIPs.tsx | 4 +++- .../NetworkRestrictions/NetworkRestrictions.tsx | 4 +++- .../DeleteProjectPanel/DeleteProjectButton.tsx | 4 +++- .../TransferProjectButton.tsx | 4 +++- .../InstanceConfiguration.tsx | 4 +++- .../InfrastructureConfiguration/MapView.tsx | 4 +++- .../layouts/ProjectLayout/PauseFailedState.tsx | 7 ++++++- .../PausedState/ProjectPausedState.tsx | 4 +++- .../layouts/ProjectLayout/RestoreFailedState.tsx | 7 ++++++- .../layouts/ProjectLayout/RestoringState.tsx | 7 ++++++- .../layouts/ReportsLayout/ReportsMenu.tsx | 4 +++- .../layouts/StorageLayout/StorageMenu.tsx | 4 +++- .../layouts/TableEditorLayout/TableEditorMenu.tsx | 4 +++- .../StorageExplorer/FileExplorerHeader.tsx | 8 ++++++-- .../FileExplorerHeaderSelection.tsx | 2 ++ apps/studio/pages/project/[ref]/auth/policies.tsx | 1 - 35 files changed, 134 insertions(+), 41 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx index 9850a9a51da..80dc0264277 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx @@ -100,7 +100,9 @@ export const HookCard = ({ hook, onSelect }: HookCardProps) => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to configure auth hooks', + text: !canUpdateAuthHook + ? 'You need additional permissions to configure auth hooks' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index b11333148e0..ba6aa9ab2ae 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -804,7 +804,9 @@ export const AIPolicyEditorPanel = memo(function ({ tooltip={{ content: { side: 'top', - text: 'You need additional permissions to update policies', + text: !canUpdatePolicies + ? 'You need additional permissions to update policies' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index ae34fb099d4..9944a44b4c4 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -3,7 +3,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import Panel from 'components/ui/Panel' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -20,7 +20,6 @@ import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, } from 'ui' -import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' interface PolicyRowProps { policy: PostgresPolicy diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index 1a954208d97..4823c3bc72e 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -79,7 +79,9 @@ const PolicyTableRowHeader = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to toggle RLS', + text: !canToggleRLS + ? 'You need additional permissions to toggle RLS' + : undefined, }, }} > @@ -94,7 +96,9 @@ const PolicyTableRowHeader = ({ content: { side: 'bottom', text: !canToggleRLS - ? 'You need additional permissions to create RLS policies' + ? !canToggleRLS + ? 'You need additional permissions to create RLS policies' + : undefined : undefined, }, }} diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx index 8ee007d54b4..304cac3af4d 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx @@ -72,7 +72,9 @@ export const RedirectUrlList = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to remove redirect URLs', + text: !canUpdateConfig + ? 'You need additional permissions to remove redirect URLs' + : undefined, }, }} icon={} @@ -88,7 +90,9 @@ export const RedirectUrlList = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to update redirect URLs', + text: !canUpdateConfig + ? 'You need additional permissions to update redirect URLs' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 7dd8d768955..6754170842f 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -521,6 +521,8 @@ const RowAction = ({ } className?: string }) => { + const disabled = button?.disabled ?? false + return (
      @@ -535,11 +537,13 @@ const RowAction = ({ icon={success ? : button.icon} loading={button.isLoading ?? false} onClick={button.onClick} - disabled={button?.disabled ?? false} + disabled={disabled} tooltip={{ content: { side: 'bottom', - text: `You need additional permissions to ${button.text.toLowerCase()}`, + text: disabled + ? `You need additional permissions to ${button.text.toLowerCase()}` + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx b/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx index 435e55c6fa4..366d9e47ff2 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchManagement.tsx @@ -216,7 +216,9 @@ const BranchManagement = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create branches', + text: !canCreateBranches + ? 'You need additional permissions to create branches' + : undefined, }, }} > @@ -271,7 +273,9 @@ const BranchManagement = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to disable branching', + text: !canDisableBranching + ? 'You need additional permissions to disable branching' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx b/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx index 4432578919c..264af8aa091 100644 --- a/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EmptyStates.tsx @@ -29,7 +29,9 @@ export const BranchingEmptyState = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to enable branching', + text: !canEnableBranching + ? 'You need additional permissions to enable branching' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx index af20da05fdd..cbb8960f1fa 100644 --- a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseAccessToken.tsx @@ -40,7 +40,9 @@ const CreateWarehouseAccessToken = ({ onSubmit, loading, open, setOpen }: Create tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create access tokens', + text: !canCreateAccessTokens + ? 'You need additional permissions to create access tokens' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 65f4bf35f2c..521ea2827c9 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -142,7 +142,9 @@ const FunctionsList = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create functions', + text: !canCreateFunctions + ? 'You need additional permissions to create functions' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx index cf3977ff8f1..b3c3482ee26 100644 --- a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -72,7 +72,9 @@ const ColumnList = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create columns', + text: !canUpdateColumns + ? 'You need additional permissions to create columns' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index 519347c7858..d7245c85ec8 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -275,7 +275,9 @@ const TableList = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create tables', + text: !canUpdateTables + ? 'You need additional permissions to create tables' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 3936710c68e..aca153eb113 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -257,7 +257,9 @@ const EdgeFunctionDetails = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to delete edge functions', + text: !canUpdateEdgeFunction + ? 'You need additional permissions to delete edge functions' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx index 516266d5c20..ba5212aafdf 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx @@ -42,7 +42,9 @@ const EdgeFunctionSecret = ({ secret, onSelectDelete }: EdgeFunctionSecretProps) side: 'bottom', text: isReservedSecret ? 'This is a reserved secret and cannot be deleted' - : 'You need additional permissions to delete edge function secrets', + : !canUpdateSecrets + ? 'You need additional permissions to delete edge function secrets' + : undefined, }, }} /> diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx index c5183483b9e..fa151ea1294 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx @@ -81,7 +81,9 @@ const EdgeFunctionSecrets = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to update edge function secrets', + text: !canUpdateSecrets + ? 'You need additional permissions to update edge function secrets' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 02391b94026..caa6249c62d 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -316,7 +316,9 @@ export const CreateCronJobSheet = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to enable database extensions', + text: !canToggleExtensions + ? 'You need additional permissions to enable database extensions' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Integrations/IntegrationPanels.tsx b/apps/studio/components/interfaces/Integrations/IntegrationPanels.tsx index dc03ab01576..8af175bded5 100644 --- a/apps/studio/components/interfaces/Integrations/IntegrationPanels.tsx +++ b/apps/studio/components/interfaces/Integrations/IntegrationPanels.tsx @@ -303,7 +303,10 @@ const EmptyIntegrationConnection = React.forwardRef< disabled={disabled} onClick={() => onClick()} tooltip={{ - content: { side: 'bottom', text: 'Additional permissions required to add connection' }, + content: { + side: 'bottom', + text: disabled ? 'Additional permissions required to add connection' : undefined, + }, }} > Add new project connection diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx index c7c5c69de5b..375284c0b9b 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx @@ -85,7 +85,9 @@ const OAuthApps = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create apps', + text: !canCreateOAuthApps + ? 'You need additional permissions to create apps' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/API/HardenAPIModal.tsx b/apps/studio/components/interfaces/Settings/API/HardenAPIModal.tsx index 038c8878f33..cabc8158278 100644 --- a/apps/studio/components/interfaces/Settings/API/HardenAPIModal.tsx +++ b/apps/studio/components/interfaces/Settings/API/HardenAPIModal.tsx @@ -184,7 +184,13 @@ export const HardenAPIModal = ({ visible, onClose }: HardenAPIModalProps) => { disabled={hasAPISchema && isAPISchemaExposed} loading={isCreatingAPISchema} tooltip={{ - content: { side: 'right', text: 'Schema has already been created and exposed' }, + content: { + side: 'right', + text: + hasAPISchema && isAPISchemaExposed + ? 'Schema has already been created and exposed' + : undefined, + }, }} > Create and expose schema to Data API @@ -249,7 +255,12 @@ export const HardenAPIModal = ({ visible, onClose }: HardenAPIModalProps) => { className="w-min" disabled={!isPublicSchemaExposed} loading={isUpdatingConfig} - tooltip={{ content: { side: 'right', text: 'Public schema no longer exposed' } }} + tooltip={{ + content: { + side: 'right', + text: !isPublicSchemaExposed ? 'Public schema no longer exposed' : undefined, + }, + }} onClick={onSelectRemovePublicSchema} > Remove public schema from exposed schemas diff --git a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx index 2c942355681..cc291744349 100644 --- a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx +++ b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx @@ -99,7 +99,9 @@ const BannedIPs = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to unban networks', + text: !canUnbanNetworks + ? 'You need additional permissions to unban networks' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx index 19a6f94a6fd..b39170c9349 100644 --- a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx +++ b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx @@ -55,7 +55,9 @@ const DisallowAllAccessButton = ({ disabled, onClick }: AccessButtonProps) => ( tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to update network restrictions', + text: disabled + ? 'You need additional permissions to update network restrictions' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx index 9025f11222e..427a7d66422 100644 --- a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx @@ -27,7 +27,9 @@ const DeleteProjectButton = ({ type = 'danger' }: DeleteProjectButtonProps) => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to delete this project', + text: !canDeleteProject + ? 'You need additional permissions to delete this project' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx index c1f6a75e03d..9777aca6547 100644 --- a/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx @@ -79,7 +79,9 @@ const TransferProjectButton = () => { side: 'bottom', text: !canTransferProject ? 'You need additional permissions to transfer this project' - : 'Project transfers are temporarily disabled, please try again later.', + : disableProjectTransfer + ? 'Project transfers are temporarily disabled, please try again later.' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index ac9c1837de8..81db395954a 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -231,7 +231,9 @@ const InstanceConfigurationUI = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to deploy replicas', + text: !canManageReplicas + ? 'You need additional permissions to deploy replicas' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx index a7ddcaaa19b..b39fe8e455c 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx @@ -358,7 +358,9 @@ const MapView = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to deploy replicas', + text: !canManageReplicas + ? 'You need additional permissions to deploy replicas' + : undefined, }, }} > diff --git a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx index 88eb1049fe9..9e8e36faf38 100644 --- a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx @@ -75,7 +75,12 @@ const PauseFailedState = () => { icon={} loading={isDownloading} disabled={backups.length === 0} - tooltip={{ content: { side: 'bottom', text: 'No available backups to download' } }} + tooltip={{ + content: { + side: 'bottom', + text: backups.length === 0 ? 'No available backups to download' : undefined, + }, + }} onClick={onClickDownloadBackup} > Download backup diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index 9c2e528eb2a..a64d8ccff44 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -276,7 +276,9 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to resume this project', + text: !canResumeProject + ? 'You need additional permissions to resume this project' + : undefined, }, }} > diff --git a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx index 8b416e8a6a0..6ba94a4833b 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx @@ -75,7 +75,12 @@ const RestoreFailedState = () => { icon={} loading={isDownloading} disabled={backups.length === 0} - tooltip={{ content: { side: 'bottom', text: 'No available backups to download' } }} + tooltip={{ + content: { + side: 'bottom', + text: backups.length === 0 ? 'No available backups to download' : undefined, + }, + }} onClick={onClickDownloadBackup} > Download backup diff --git a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx index 4f95cd0f2c4..5f4bf20491e 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -126,7 +126,12 @@ const RestoringState = () => { icon={} loading={isDownloading} disabled={backups.length === 0} - tooltip={{ content: { side: 'bottom', text: 'No available backups to download' } }} + tooltip={{ + content: { + side: 'bottom', + text: backups.length === 0 ? 'No available backups to download' : undefined, + }, + }} onClick={onClickDownloadBackup} > Download backup diff --git a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx index a4e768de272..dbb19165499 100644 --- a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx +++ b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx @@ -136,7 +136,9 @@ const ReportsMenu = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create custom reports', + text: !canCreateCustomReport + ? 'You need additional permissions to create custom reports' + : undefined, }, }} > diff --git a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx index a30790de3b6..d47cd875f5f 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -79,7 +79,9 @@ const StorageMenu = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create buckets', + text: !canCreateBuckets + ? 'You need additional permissions to create buckets' + : undefined, }, }} > diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 82ff2ae0376..215893ac1d3 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -136,7 +136,9 @@ const TableEditorMenu = () => { tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create tables', + text: !canCreateTables + ? 'You need additional permissions to create tables' + : undefined, }, }} > diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index 5e31b0477ca..e9b7bf980c9 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -434,7 +434,9 @@ const FileExplorerHeader = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to upload files', + text: !canUpdateStorage + ? 'You need additional permissions to upload files' + : undefined, }, }} > @@ -448,7 +450,9 @@ const FileExplorerHeader = ({ tooltip={{ content: { side: 'bottom', - text: 'You need additional permissions to create folders', + text: !canUpdateStorage + ? 'You need additional permissions to create folders' + : undefined, }, }} > diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index c1131dd741b..8b6daad9cf2 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -48,6 +48,7 @@ const FileExplorerHeaderSelection = () => { } type="primary" + disabled={!canUpdateFiles} onClick={() => setSelectedItemsToDelete(selectedItems)} tooltip={{ content: { @@ -62,6 +63,7 @@ const FileExplorerHeaderSelection = () => { } type="primary" + disabled={!canUpdateFiles} onClick={() => setSelectedItemsToMove(selectedItems)} tooltip={{ content: { diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index 1c3bb93b5a8..1d13326de51 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -9,7 +9,6 @@ import Policies from 'components/interfaces/Auth/Policies/Policies' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import NoPermission from 'components/ui/NoPermission' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' From bb0306061d63cee89ae2a64731c28f5ce94d706f Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 24 Oct 2024 12:56:20 -0230 Subject: [PATCH 43/80] Chore/update to next 15 (#30075) * Update Next.js docs for v15 * More awaiting * remove async for pages router * fix for dashboard connection guide --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- apps/docs/DEVELOPERS.md | 2 +- apps/docs/README.md | 2 +- apps/docs/components/MDX/oauth_pkce_flow.mdx | 2 +- .../docs/content/guides/auth/auth-helpers/nextjs.mdx | 4 ++-- apps/docs/content/guides/auth/passwords.mdx | 4 ++-- apps/docs/content/guides/auth/quickstarts/nextjs.mdx | 9 ++++++--- .../guides/auth/server-side/creating-a-client.mdx | 4 ++-- apps/docs/content/guides/auth/server-side/nextjs.mdx | 12 ++++++------ .../content/guides/auth/social-login/auth-google.mdx | 4 ++-- .../guides/getting-started/quickstarts/nextjs.mdx | 8 ++++---- .../guides/getting-started/tutorials/with-nextjs.mdx | 8 ++++---- .../content/nextjs/app/supabasejs/content.tsx | 2 +- 12 files changed, 32 insertions(+), 29 deletions(-) diff --git a/apps/docs/DEVELOPERS.md b/apps/docs/DEVELOPERS.md index ed8fa91080d..2d9f7cb3684 100644 --- a/apps/docs/DEVELOPERS.md +++ b/apps/docs/DEVELOPERS.md @@ -14,7 +14,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE ## Local setup -[supabase.com/docs](https://supabase.com/docs) is a Next.JS site. You can get setup by following the same steps for all of our other Next.JS projects: +[supabase.com/docs](https://supabase.com/docs) is a Next.js site. You can get setup by following the same steps for all of our other Next.js projects: 1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) 2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false` diff --git a/apps/docs/README.md b/apps/docs/README.md index c8a0cd91609..7803882d6c7 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -31,7 +31,7 @@ We use documentation specifications which can be used to generate human-readable - CLISpec (custom to Supabase): for CLI commands and usage. The benefit of using custom specifications is that we can generate many other types from a strict schema (eg, HTML and manpages). -It also means that we can switch to any documentation system we want. On this site we use Next.JS, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool. +It also means that we can switch to any documentation system we want. On this site we use Next.js, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool. ## Contributing diff --git a/apps/docs/components/MDX/oauth_pkce_flow.mdx b/apps/docs/components/MDX/oauth_pkce_flow.mdx index f4bb7617a45..02beff87623 100644 --- a/apps/docs/components/MDX/oauth_pkce_flow.mdx +++ b/apps/docs/components/MDX/oauth_pkce_flow.mdx @@ -68,7 +68,7 @@ export async function GET(request: Request) { const next = searchParams.get('next') ?? '/' if (code) { - const supabase = createClient() + const supabase = await createClient() const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer diff --git a/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx index b0f1f3cbd30..5b64c6a4f0e 100644 --- a/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx @@ -20,7 +20,7 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W size="medium" className="text-foreground-light border-b mt-8 pb-2" > - + ({ cookies: () => cookieStore }) await supabase.auth.exchangeCodeForSession(code) } diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index c671ac333dc..9d3d0f83bf1 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -184,7 +184,7 @@ export async function GET(request: NextRequest) { redirectTo.pathname = next if (token_hash && type) { - const supabase = createClient() + const supabase = await createClient() const { error } = await supabase.auth.verifyOtp({ type, @@ -648,7 +648,7 @@ export async function GET(request: NextRequest) { redirectTo.pathname = next if (token_hash && type) { - const supabase = createClient() + const supabase = await createClient() const { error } = await supabase.auth.verifyOtp({ type, diff --git a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx index 9a7117247af..6db4230d2ac 100644 --- a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx @@ -72,9 +72,7 @@ hideToc: true Start the development server, go to http://localhost:3000 in a browser, and you should see the contents of `app/page.tsx`. - To Sign Up a new user, navigate to http://localhost:3000/login, and click `Sign Up Now`. - - Check out the [Supabase Auth docs](https://supabase.com/docs/guides/auth#authentication) for more authentication methods. + To sign up a new user, navigate to http://localhost:3000/login, and click `Sign Up Now`. @@ -88,3 +86,8 @@ hideToc: true + +## Learn more + +- [Setting up Server-Side Auth for Next.js](https://supabase.com/docs/guides/auth/server-side/nextjs) for a Next.js deep dive +- [Supabase Auth docs](https://supabase.com/docs/guides/auth#authentication) for more Supabase authentication methods diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index b9e5d752c51..7d8e3f0ecc3 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -171,8 +171,8 @@ export function createClient() { import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' -export function createClient() { - const cookieStore = cookies() +export async function createClient() { + const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index 384f78b869a..8189371cc26 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -125,8 +125,8 @@ export function createClient() { import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' -export function createClient() { - const cookieStore = cookies() +export async function createClient() { + const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -337,7 +337,7 @@ import { redirect } from 'next/navigation' import { createClient } from '@/utils/supabase/server' export async function login(formData: FormData) { - const supabase = createClient() + const supabase = await createClient() // type-casting here for convenience // in practice, you should validate your inputs @@ -357,7 +357,7 @@ export async function login(formData: FormData) { } export async function signup(formData: FormData) { - const supabase = createClient() + const supabase = await createClient() // type-casting here for convenience // in practice, you should validate your inputs @@ -429,7 +429,7 @@ export async function GET(request: NextRequest) { const next = searchParams.get('next') ?? '/' if (token_hash && type) { - const supabase = createClient() + const supabase = await createClient() const { error } = await supabase.auth.verifyOtp({ type, @@ -482,7 +482,7 @@ import { redirect } from 'next/navigation' import { createClient } from '@/utils/supabase/server' export default async function PrivatePage() { - const supabase = createClient() + const supabase = await createClient() const { data, error } = await supabase.auth.getUser() if (error || !data?.user) { diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index 8ebb31ea2c3..c79d6d15300 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -341,9 +341,9 @@ Most web apps and websites can utilize Google's [personalized sign-in buttons](h // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method ``` -### One-tap with NextJS +### One-tap with Next.js -If you're integrating Google One-Tap with your NextJS application, you can refer to the example below to get started: +If you're integrating Google One-Tap with your Next.js application, you can refer to the example below to get started: ```tsx 'use client' diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index 720ea4966e5..1b11196eaeb 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -8,7 +8,7 @@ hideToc: true - + Go to [database.new](https://database.new) and create a new Supabase project. @@ -118,7 +118,7 @@ hideToc: true import { createClient } from '@/utils/supabase/server'; export default async function Notes() { - const supabase = createClient(); + const supabase = await createClient(); const { data: notes } = await supabase.from("notes").select(); return
      {JSON.stringify(notes, null, 2)}
      @@ -129,8 +129,8 @@ hideToc: true import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' - export function createClient() { - const cookieStore = cookies() + export async function createClient() { + const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx index b849b2b1af0..02d0c095791 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx @@ -385,7 +385,7 @@ import { redirect } from 'next/navigation' import { createClient } from '@/utils/supabase/server' export async function login(formData) { - const supabase = createClient() + const supabase = await createClient() // type-casting here for convenience // in practice, you should validate your inputs @@ -405,7 +405,7 @@ export async function login(formData) { } export async function signup(formData) { - const supabase = createClient() + const supabase = await createClient() const data = { email: formData.get('email'), @@ -512,7 +512,7 @@ export async function GET(request) { redirectTo.searchParams.delete('type') if (token_hash && type) { - const supabase = createClient() + const supabase = await createClient() const { error } = await supabase.auth.verifyOtp({ type, @@ -720,7 +720,7 @@ import AccountForm from './account-form' import { createClient } from '@/utils/supabase/server' export default async function Account() { - const supabase = createClient() + const supabase = await createClient() const { data: { user }, diff --git a/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx b/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx index 9a2cd70d26c..2ff11fda1fc 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx @@ -35,7 +35,7 @@ import { createClient } from '@/utils/supabase/server' import { cookies } from 'next/headers' export default async function Page() { - const cookieStore = cookies() + const cookieStore = await cookies() const supabase = createClient(cookieStore) const { data: todos } = await supabase.from('todos').select() From 9a3d0992c304f70dc0b83146a41bfda8f935f6a3 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 24 Oct 2024 17:38:51 +0200 Subject: [PATCH 44/80] www features section (#30072) * init features page * fix filtering * seo boostg * add features to sitemap * update copywriting * improve features descriptions * more studio features * moar featuress * better back to features * prev next feature nav * clean * improve page footer * fixes * stuff * sync filters with query params on page load * better mobile layout * 16px mobile input fontsize * add cover images or videos to features * improve a11y * update sitemap * last changes * update schema definitions --- .../components/Blog/ShareArticleActions.tsx | 6 +- apps/www/components/Nav/MobileMenu.tsx | 46 +- apps/www/components/Nav/ProductDropdown.tsx | 94 +- apps/www/components/PrevNextFeatureNav.tsx | 132 ++ apps/www/data/features.tsx | 1651 +++++++++++++++++ apps/www/internals/generate-sitemap.mjs | 2 + apps/www/pages/features.tsx | 219 +++ apps/www/pages/features/[slug].tsx | 237 +++ .../auth-captcha-protection-light.png | Bin 0 -> 59792 bytes .../features/auth-captcha-protection.png | Bin 0 -> 57927 bytes .../auto-generated-graphql-api-light.png | Bin 0 -> 81202 bytes .../features/auto-generated-graphql-api.png | Bin 0 -> 81134 bytes .../auto-generated-rest-api-light.png | Bin 0 -> 95645 bytes .../features/auto-generated-rest-api.png | Bin 0 -> 78493 bytes .../public/images/features/backups-light.png | Bin 0 -> 91181 bytes apps/www/public/images/features/backups.png | Bin 0 -> 79187 bytes .../images/features/branching-light.png | Bin 0 -> 91873 bytes apps/www/public/images/features/branching.png | Bin 0 -> 85515 bytes .../images/features/custom-domains-light.png | Bin 0 -> 75602 bytes .../public/images/features/custom-domains.png | Bin 0 -> 75689 bytes .../features/database-webhooks-light.png | Bin 0 -> 69306 bytes .../images/features/database-webhooks.png | Bin 0 -> 64094 bytes .../images/features/email-login-light.png | Bin 0 -> 69914 bytes .../public/images/features/email-login.png | Bin 0 -> 69560 bytes .../images/features/features-cover-dark.svg | 502 +++++ .../images/features/features-cover-light.svg | 500 +++++ .../images/features/file-storage-light.png | Bin 0 -> 139800 bytes .../public/images/features/file-storage.png | Bin 0 -> 102780 bytes .../images/features/logs-analytics-light.png | Bin 0 -> 96306 bytes .../public/images/features/logs-analytics.png | Bin 0 -> 92413 bytes .../images/features/management-api-light.png | Bin 0 -> 97950 bytes .../public/images/features/management-api.png | Bin 0 -> 98174 bytes .../features/network-restrictions-light.png | Bin 0 -> 86632 bytes .../images/features/network-restrictions.png | Bin 0 -> 77791 bytes .../images/features/phone-login-light.png | Bin 0 -> 63663 bytes .../public/images/features/phone-login.png | Bin 0 -> 60087 bytes .../features/policy-templates-light.png | Bin 0 -> 112998 bytes .../images/features/policy-templates.png | Bin 0 -> 114513 bytes .../features/postgres-database-light.png | Bin 0 -> 103920 bytes .../images/features/postgres-database.png | Bin 0 -> 90251 bytes .../features/postgres-extensions-light.png | Bin 0 -> 90787 bytes .../images/features/postgres-extensions.png | Bin 0 -> 89450 bytes .../images/features/postgres-roles-light.png | Bin 0 -> 87536 bytes .../public/images/features/postgres-roles.png | Bin 0 -> 89088 bytes .../features/reports-and-metrics-light.png | Bin 0 -> 70075 bytes .../images/features/reports-and-metrics.png | Bin 0 -> 74068 bytes .../features/s3-compatibility-light.png | Bin 0 -> 100608 bytes .../images/features/s3-compatibility.png | Bin 0 -> 92213 bytes ...security-and-performance-advisor-light.png | Bin 0 -> 93851 bytes .../security-and-performance-advisor.png | Bin 0 -> 86208 bytes .../images/features/social-login-light.png | Bin 0 -> 55196 bytes .../public/images/features/social-login.png | Bin 0 -> 54314 bytes .../images/features/sql-editor-light.png | Bin 0 -> 94977 bytes .../www/public/images/features/sql-editor.png | Bin 0 -> 84239 bytes .../images/features/ssl-enforcement-light.png | Bin 0 -> 83625 bytes .../images/features/ssl-enforcement.png | Bin 0 -> 73480 bytes .../features/terraform-provider-light.png | Bin 0 -> 58337 bytes .../images/features/terraform-provider.png | Bin 0 -> 57843 bytes .../features/visual-schema-designer-light.png | Bin 0 -> 104309 bytes .../features/visual-schema-designer.png | Bin 0 -> 91362 bytes apps/www/public/rss.xml | 9 +- apps/www/public/sitemap_www.xml | 948 +++++++++- 62 files changed, 4298 insertions(+), 48 deletions(-) create mode 100644 apps/www/components/PrevNextFeatureNav.tsx create mode 100644 apps/www/data/features.tsx create mode 100644 apps/www/pages/features.tsx create mode 100644 apps/www/pages/features/[slug].tsx create mode 100644 apps/www/public/images/features/auth-captcha-protection-light.png create mode 100644 apps/www/public/images/features/auth-captcha-protection.png create mode 100644 apps/www/public/images/features/auto-generated-graphql-api-light.png create mode 100644 apps/www/public/images/features/auto-generated-graphql-api.png create mode 100644 apps/www/public/images/features/auto-generated-rest-api-light.png create mode 100644 apps/www/public/images/features/auto-generated-rest-api.png create mode 100644 apps/www/public/images/features/backups-light.png create mode 100644 apps/www/public/images/features/backups.png create mode 100644 apps/www/public/images/features/branching-light.png create mode 100644 apps/www/public/images/features/branching.png create mode 100644 apps/www/public/images/features/custom-domains-light.png create mode 100644 apps/www/public/images/features/custom-domains.png create mode 100644 apps/www/public/images/features/database-webhooks-light.png create mode 100644 apps/www/public/images/features/database-webhooks.png create mode 100644 apps/www/public/images/features/email-login-light.png create mode 100644 apps/www/public/images/features/email-login.png create mode 100644 apps/www/public/images/features/features-cover-dark.svg create mode 100644 apps/www/public/images/features/features-cover-light.svg create mode 100644 apps/www/public/images/features/file-storage-light.png create mode 100644 apps/www/public/images/features/file-storage.png create mode 100644 apps/www/public/images/features/logs-analytics-light.png create mode 100644 apps/www/public/images/features/logs-analytics.png create mode 100644 apps/www/public/images/features/management-api-light.png create mode 100644 apps/www/public/images/features/management-api.png create mode 100644 apps/www/public/images/features/network-restrictions-light.png create mode 100644 apps/www/public/images/features/network-restrictions.png create mode 100644 apps/www/public/images/features/phone-login-light.png create mode 100644 apps/www/public/images/features/phone-login.png create mode 100644 apps/www/public/images/features/policy-templates-light.png create mode 100644 apps/www/public/images/features/policy-templates.png create mode 100644 apps/www/public/images/features/postgres-database-light.png create mode 100644 apps/www/public/images/features/postgres-database.png create mode 100644 apps/www/public/images/features/postgres-extensions-light.png create mode 100644 apps/www/public/images/features/postgres-extensions.png create mode 100644 apps/www/public/images/features/postgres-roles-light.png create mode 100644 apps/www/public/images/features/postgres-roles.png create mode 100644 apps/www/public/images/features/reports-and-metrics-light.png create mode 100644 apps/www/public/images/features/reports-and-metrics.png create mode 100644 apps/www/public/images/features/s3-compatibility-light.png create mode 100644 apps/www/public/images/features/s3-compatibility.png create mode 100644 apps/www/public/images/features/security-and-performance-advisor-light.png create mode 100644 apps/www/public/images/features/security-and-performance-advisor.png create mode 100644 apps/www/public/images/features/social-login-light.png create mode 100644 apps/www/public/images/features/social-login.png create mode 100644 apps/www/public/images/features/sql-editor-light.png create mode 100644 apps/www/public/images/features/sql-editor.png create mode 100644 apps/www/public/images/features/ssl-enforcement-light.png create mode 100644 apps/www/public/images/features/ssl-enforcement.png create mode 100644 apps/www/public/images/features/terraform-provider-light.png create mode 100644 apps/www/public/images/features/terraform-provider.png create mode 100644 apps/www/public/images/features/visual-schema-designer-light.png create mode 100644 apps/www/public/images/features/visual-schema-designer.png diff --git a/apps/www/components/Blog/ShareArticleActions.tsx b/apps/www/components/Blog/ShareArticleActions.tsx index b95a64a94d1..284080999bb 100644 --- a/apps/www/components/Blog/ShareArticleActions.tsx +++ b/apps/www/components/Blog/ShareArticleActions.tsx @@ -1,22 +1,24 @@ import Link from 'next/link' -import { IconLinkedinSolid, IconTwitterX, IconYCombinator } from 'ui' +import { cn, IconLinkedinSolid, IconTwitterX, IconYCombinator } from 'ui' const ShareArticleActions = ({ title, slug, iconSize = 20, basePath = 'https://supabase.com/blog/', + className, }: { title: string slug: string iconSize?: number basePath?: string + className?: string }) => { const permalink = encodeURIComponent(`${basePath}${slug}`) const encodedTitle = encodeURIComponent(title) return ( -
      +
      { const AccordionMenuItem = ({ menuItem }: any) => ( <> {menuItem.title === 'Product' ? ( - Object.values(menuItem.subMenu)?.map((component: any) => ( - - )) + <> + {Object.values(menuItem.subMenu)?.map((component: any) => ( + + ))} + +
      + Features + + Explore everything you can do with Supabase. + +
      + + + ) : menuItem.title === 'Developers' ? (
      {menuItem.subMenu['navigation'].map((column: any) => ( @@ -70,7 +96,7 @@ const MobileMenu = ({ open, setOpen, isDarkMode, menu }: Props) => { ))}
      -
    -
    -
    +
    +
    - Customer Stories - +
    +
    + +
    +
    + Features + + Explore everything you can do with Supabase. + +
    +
    + -
      - {CustomersData.slice(0, isTablet ? 2 : 3).map((customer) => ( -
    • - -
      - {customer.title} -
      -
      -

      - {customer.title} -

      -
      - -
    • - ))} -
    +
    + + Customer Stories + + +
      + {CustomersData.slice(0, isTablet ? 2 : 2).map((customer) => ( +
    • + +
      + {customer.title} +
      +
      +

      + {customer.title} +

      +
      + +
    • + ))} +
    +
    -

    +

    {ComparisonsData.label}

      diff --git a/apps/www/components/PrevNextFeatureNav.tsx b/apps/www/components/PrevNextFeatureNav.tsx new file mode 100644 index 00000000000..bc47384ad43 --- /dev/null +++ b/apps/www/components/PrevNextFeatureNav.tsx @@ -0,0 +1,132 @@ +import { PropsWithChildren, useEffect, useState } from 'react' +import Link, { LinkProps } from 'next/link' +import { useRouter } from 'next/router' +import { ArrowLeft, ArrowRight, List } from 'lucide-react' +import { + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' + +import { type FeatureType, features } from '~/data/features' + +interface Props { + className?: string + wrapperClassName?: string + currentFeature: FeatureType + prevLink: string + nextLink: string +} + +const buttonClassName = + 'relative z-10 flex items-center gap-1 px-2 pointer-events-auto overflow-hidden !h-[30px] !min-w-[30px] !max-w-[30px] py-1 justify-center rounded-full border bg-default hover:bg-surface-100 hover:text-foreground hover:border-foreground-lighter transition-all' +const iconClassName = 'className="w-4 h-4 flex-shrink-0' + +const PrevNextFeatureNav: React.FC = ({ + className, + wrapperClassName, + currentFeature, + prevLink, + nextLink, + ...props +}) => { + const [open, setOpen] = useState(false) + const router = useRouter() + + useEffect(() => { + function handleRouteChange() { + setOpen(false) + } + + router.events.on('routeChangeComplete', handleRouteChange) + return () => { + router.events.off('routeChangeComplete', handleRouteChange) + } + }, [router.events]) + + return ( +
      +
      + + + Previous feature + + + + + + + + + All Features + + + + + {features + .sort((a, b) => { + if (a['title'] < b['title']) return -1 + if (a['title'] > b['title']) return 1 + return 0 + }) + .map((feature) => ( + + + + {feature.title} + + + ))} + + + + + Next feature + + +
      +
      + ) +} + +interface ButtonLinkProps extends LinkProps { + className?: string +} + +const ButtonLink: React.FC> = ({ + href, + className, + children, +}) => { + return ( + + {children} + + ) +} + +export default PrevNextFeatureNav diff --git a/apps/www/data/features.tsx b/apps/www/data/features.tsx new file mode 100644 index 00000000000..6d582b9df6c --- /dev/null +++ b/apps/www/data/features.tsx @@ -0,0 +1,1651 @@ +import { + ChartScatter, + Database, + FileCode2, + Lock, + Cloud, + UploadCloud, + Image, + MessageCircle, + ShieldCheck, + Package, + Users, + Folders, + RectangleEllipsis, + Braces, + Globe, + UserX, + GitBranch, + DatabaseZap, + Mail, + Smartphone, + Server, + Activity, + BarChart, + Brain, + Puzzle, + DatabaseBackup, + ShieldPlus, + Terminal, + Zap, +} from 'lucide-react' +import { PRODUCT, PRODUCT_SHORTNAMES } from 'shared-data/products' +import type { LucideIcon } from 'lucide-react' + +enum ADDITIONAL_PRODUCTS { + PLATFORM = 'platform management', + STUDIO = 'studio', +} + +export type FeatureProductType = PRODUCT | ADDITIONAL_PRODUCTS + +export type FeatureType = { + /** + * name of the feature + */ + title: string + /** + * subtitle will be displayed in the feature card, after the title + */ + subtitle: string + /** + * The body content in the feature page + */ + description: string + /** + * slug of the feature page + */ + slug: string + /** + * icon will be displayed in the feature card + */ + icon: string | LucideIcon + /** + * Each feature belongs to one or more products + */ + products: FeatureProductType[] + /** + * heroImage can be an absolute path to either: + * - an image + * - a youtube video + * - a video sourced from supabase storage + */ + heroImage: string + /** + * light-mode version image, if heroImage is not a video + */ + heroImageLight?: string + /** + * url to docs or blog page for this feature + */ + docsUrl?: string +} + +export const features: FeatureType[] = [ + // Database + { + title: 'Postgres database', + subtitle: 'Every project is a full Postgres database.', + description: ` +Every Supabase project is a dedicated [Postgres database](https://www.postgresql.org/) - _"a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance"_. + +It's 100% portable, which means you can easily migrate your data to and from other Postgres databases, ensuring that your data is never locked into a proprietary system. + +## Key benefits +1. Advanced data types: Utilize JSON, arrays, and custom types to store complex data structures efficiently. +2. Powerful indexing: Improve query performance with various indexing options, including B-tree, Hash, and GiST. +3. Full SQL support: Execute complex queries and leverage advanced SQL features for data manipulation and analysis. +4. ACID compliance: Ensure data integrity and consistency with Postgres's transactional capabilities. +5. Extensibility: Add custom functions and extensions to extend database functionality. +6. Scalability: Handle growing data volumes and user loads with Postgres's proven scalability. +7. Community support: Benefit from a large, active community and extensive documentation. +`, + icon: Database, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/postgres-database.png', + heroImageLight: '/images/features/postgres-database-light.png', + docsUrl: 'https://supabase.com/docs/guides/database/overview', + slug: 'postgres-database', + }, + { + title: 'Vector database', + subtitle: 'Store vector embeddings right next to the rest of your data.', + description: ` +Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale. + +## Key benefits +1. Unified data storage: Keep vector embeddings and relational data in one place, simplifying your data architecture. +2. Efficient similarity search: Perform fast and accurate similarity searches on high-dimensional data. +3. Seamless integration: Easily incorporate vector search into existing applications without additional infrastructure. +4. Scalability: Handle large volumes of vector data with Postgres's proven scalability. +5. Cost-effective: Eliminate the need for separate vector databases, reducing infrastructure costs. +6. Advanced querying: Combine vector similarity search with traditional SQL queries for powerful hybrid searches. +7. Real-time updates: Continuously update and query vector data without complex ETL processes. +`, + icon: ChartScatter, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: 'https://www.youtube-nocookie.com/embed/ibzlEQmgPPY', + docsUrl: 'https://supabase.com/docs/guides/ai', + slug: 'vector-database', + }, + { + title: 'Auto-generated REST API via PostgREST', + subtitle: 'RESTful APIs auto-generated from your database.', + description: ` +Supabase automatically generates a comprehensive RESTful API from your database schema, powered by PostgREST. This feature dramatically accelerates development by eliminating the need to write boilerplate server-side code for basic CRUD operations. + +## Key benefits +1. Rapid development: Instantly access your data through a RESTful API without writing any backend code. +2. Automatic updates: API endpoints automatically reflect changes in your database schema, ensuring consistency. +3. Flexible querying: Use powerful query parameters to filter, sort, and paginate data directly from API calls. +4. Custom operations: Easily expose custom database functions as API endpoints for complex operations. +5. Secure by default: Leverage Postgres's role-based access control for built-in API security. +6. Performance: Benefit from PostgREST's optimized query execution for efficient API responses. +7. Standardized interface: Work with a well-documented, consistent API across all your database tables. +`, + icon: FileCode2, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/auto-generated-rest-api.png', + heroImageLight: '/images/features/auto-generated-rest-api-light.png', + docsUrl: 'https://supabase.com/docs/guides/api#rest-api-overview', + slug: 'auto-generated-rest-api', + }, + { + title: 'Auto-generated GraphQL API via pg_graphql', + subtitle: 'Fast GraphQL APIs using our custom Postgres GraphQL extension.', + description: ` +Supabase offers lightning-fast GraphQL APIs through its custom-built Postgres GraphQL extension, _pg_graphql_. + +## Key benefits +1. Simplified data fetching: Retrieve exactly the data you need in a single request, reducing over-fetching and under-fetching. +2. Automatic schema generation: GraphQL schema is automatically derived from your Postgres schema, ensuring consistency. +3. Real-time updates: Combine with Supabase's real-time features for live data subscriptions. +4. Improved performance: Leverage Postgres's query optimization capabilities for efficient GraphQL resolvers. +5. Type safety: Benefit from GraphQL's strong typing system, reducing runtime errors and improving developer experience. +6. Flexible querying: Allow clients to request complex, nested data structures in a single query. +7. Built-in documentation: Utilize GraphQL's introspection feature for self-documenting APIs. + +## The auto-generated GraphQL API is particularly beneficial for: +- Single-page applications (SPAs) requiring efficient data loading +- Mobile apps needing to minimize data transfer +- Complex UIs with varying data requirements +- Projects requiring rapid iteration on data models +- Applications with deeply nested data structures +`, + icon: Braces, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/auto-generated-graphql-api.png', + heroImageLight: '/images/features/auto-generated-graphql-api-light.png', + docsUrl: 'https://supabase.com/docs/guides/graphql/api', + slug: 'auto-generated-graphql-api', + }, + { + title: 'Database backups', + subtitle: 'Projects are backed up daily with Point in Time recovery options.', + description: ` +Supabase offers comprehensive database backup solutions to protect your data and ensure business continuity. These include daily backups and Point-in-Time Recovery (PITR), providing robust safeguards against various disaster scenarios. + +## Key features +1. Daily backups: Automatically created for all Pro, Team, and Enterprise plans. + - Pro: 7 days retention + - Team: 14 days retention + - Enterprise: Up to 30 days retention +2. Point-in-Time Recovery (PITR): Available as an add-on for finer granularity. + - Allows restoration to any point within seconds + - Achieves a Recovery Point Objective (RPO) of two minutes in worst-case scenarios +3. Backup types: + - Logical backups: Used for smaller databases (0-15GB) + - Physical backups: Used for larger databases (>15GB) and projects with PITR or read replicas +4. Flexible restoration options: Restore to a specific daily backup or a precise moment with PITR. +5. Automated process: Backups are managed and monitored by the Supabase team. + +## Database backups are crucial for: +- Protecting against accidental data loss or corruption +- Recovering from infrastructure failures +- Meeting regulatory compliance requirements +- Facilitating data migration or cloning for development + +Supabase's backup solutions provide a balance of security, flexibility, and ease of use, ensuring your data remains protected and recoverable as your project grows. +`, + icon: DatabaseBackup, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/backups.png', + heroImageLight: '/images/features/backups-light.png', + docsUrl: 'https://supabase.com/docs/guides/platform/backups', + slug: 'database-backups', + }, + { + title: 'Custom domains', + subtitle: 'White-label the Supabase APIs for a branded experience.', + description: ` +Supabase's custom domain feature allows you to use your own domain for Supabase services, enhancing your application's branding and professionalism. Available as an add-on for paid plans, this feature offers two options: custom domains and vanity subdomains. + +## Benefits: +- Branded experience for OAuth consent screens and API endpoints +- Improved portability between Supabase projects +- Consistent domain usage across development, staging, and production + +## Custom domains are particularly useful for: +- Applications using Supabase Auth with OAuth +- APIs for third-party integrations or webhooks +- Long-term API versioning and management + +By using custom domains, you create a more cohesive brand experience and gain flexibility in managing your application's infrastructure. +`, + icon: Globe, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/custom-domains.png', + heroImageLight: '/images/features/custom-domains-light.png', + docsUrl: 'https://supabase.com/docs/guides/platform/custom-domains', + slug: 'custom-domains', + }, + { + title: 'Network restrictions', + subtitle: 'Restrict IP ranges that can connect to your database.', + description: ` +Supabase's Network Restrictions feature allows you to control which IP addresses or ranges can access your database, enhancing your application's security and helping you meet compliance requirements. + +## Key features +1. IP-based access control: Limit database connections to specific IPv4 and IPv6 ranges. +2. Flexible configuration: Easily manage restrictions via Dashboard or CLI. +3. Environment-specific rules: Set different access policies for development, staging, and production. +4. Pre-authentication barrier: Restrictions are enforced before database credential checks. + +## Benefits: +- Enhanced security: Reduce the attack surface by limiting access to trusted IP addresses. +- Compliance support: Meet regulatory requirements for data access control in various industries. +- Granular control: Tailor access rules to your specific network infrastructure needs. +- Additional protection layer: Mitigate risks even if database credentials are compromised. + +## Network Restrictions are particularly valuable for: +- Enterprise applications handling sensitive data +- Financial services and healthcare systems with strict compliance requirements +- Government and public sector projects needing stringent security measures +- Multi-tenant SaaS platforms isolating client data access + +By implementing Network Restrictions, you create a more secure environment for your data, demonstrating a commitment to protection that builds trust with users and stakeholders. This feature provides a crucial first line of defense against unauthorized access attempts, complementing other security measures like strong authentication and encryption. +`, + icon: UserX, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/network-restrictions.png', + heroImageLight: '/images/features/network-restrictions-light.png', + docsUrl: 'https://supabase.com/docs/guides/platform/network-restrictions', + slug: 'network-restrictions', + }, + { + title: 'SSL enforcement', + subtitle: 'Enforce secure connections to your Postgres clients.', + description: ` +Supabase's Postgres SSL Enforcement feature allows you to mandate SSL connections to your database, enhancing security by encrypting data in transit and protecting against potential attacks. + +## Key features +1. Optional enforcement: Enable or disable SSL requirements to balance security and client compatibility. +2. Comprehensive protection: Applies to both direct Postgres and connection pooler (Supavisor) connections. +3. Flexible configuration: Manage settings via Dashboard or CLI. +4. Support for various SSL modes: Including the highly secure 'verify-full' mode. + +## Benefits: +- Data encryption: Protect all information transmitted between your application and the database. +- Integrity assurance: Prevent data tampering during transmission. +- Authentication: Ensure connections are made to the genuine database server. +- Compliance support: Meet industry standards and regulatory requirements for data protection. +- Simplified security: Automatically enforce best practices for secure database connections. + +## SSL Enforcement is particularly valuable for: +- Applications handling sensitive user data +- Financial services requiring secure transactions +- Healthcare applications managing patient information +- E-commerce platforms processing payment data +- Any application prioritizing data privacy and integrity + +By enabling SSL Enforcement, you implement a fundamental best practice in data protection. This feature ensures that every interaction with your database is encrypted and secure, whether from application servers, admin tools, or API calls. +`, + icon: ShieldCheck, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/ssl-enforcement.png', + heroImageLight: '/images/features/ssl-enforcement-light.png', + docsUrl: 'https://supabase.com/docs/guides/platform/ssl-enforcement', + slug: 'ssl-enforcement', + }, + { + title: 'Branching', + subtitle: 'Test and preview changes using Supabase Branches.', + description: ` +Supabase Branching allows you to create and test changes in separate, temporary environments without affecting your production setup. This feature works similarly to Git branches, enabling safe experimentation with configurations, database schemas, and new features. + +## Key features +1. Git-based workflow: Integrates with GitHub, creating preview branches for each pull request. +2. Isolated environments: Each branch has its own Supabase instance with separate API credentials. +3. Automatic migrations: Runs new migrations when changes are pushed to the ./supabase/migrations directory. +4. Data seeding: Preview branches can be seeded with sample data using ./supabase/seed.sql. +5. CI/CD integration: Supports preview deployments with hosting providers like Vercel. + +## Benefits: +- Risk-free experimentation: Test changes without affecting the production environment. +- Improved collaboration: Multiple team members can work on different features simultaneously. +- Streamlined reviews: Facilitate thorough checks of database changes before merging. +- Rapid iteration: Quickly prototype and validate database-driven features. + +## Supabase Branching is valuable for: +- Agile teams working on multiple features concurrently +- Projects with complex database schemas requiring careful management +- Applications undergoing significant refactoring or upgrades +- CI/CD pipelines integrating database changes +`, + icon: GitBranch, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/branching.png', + heroImageLight: '/images/features/branching-light.png', + docsUrl: 'https://supabase.com/docs/guides/platform/branching', + slug: 'branching', + }, + { + title: 'Terraform provider', + subtitle: 'Manage Supabase infrastructure via Terraform.', + description: ` +The Supabase Terraform provider enables developers and DevOps teams to manage their Supabase infrastructure as code. This integration brings the benefits of Infrastructure as Code (IaC) to your database and backend services, allowing for version-controlled, reproducible, and scalable management of Supabase resources. + +## Key features +1. Resource management: Define and control database resources, authentication settings, storage buckets, and API configurations. +2. Project-level control: Manage project settings and configurations through code. +3. Integration with existing workflows: Seamlessly incorporate Supabase management into your existing Terraform setups. +4. Import capability: Easily import existing Supabase projects into your Terraform state. + +## Benefits: +- Version control: Track and manage infrastructure changes alongside application code. +- Reproducibility: Easily recreate entire environments for development, staging, and production. +- Automated provisioning: Streamline the setup of new projects or environments. +- Consistency: Ensure uniformity across all environments, reducing configuration discrepancies. +- Scalability: Effortlessly manage multiple Supabase projects across various teams or clients. +- Collaboration: Improve team coordination by reviewing infrastructure changes through pull requests. + +## The Terraform provider is particularly valuable for: +- Enterprise-level projects requiring strict infrastructure governance +- DevOps teams implementing continuous deployment pipelines +- Multi-environment setups (development, staging, production) +- Agencies or consultancies managing multiple client projects +- Open-source projects aiming for easy contributor onboarding + +By adopting the Supabase Terraform provider, teams can implement GitOps practices, automate environment creation, easily roll back changes, and audit infrastructure modifications over time. This approach leads to more reliable, maintainable, and scalable backend infrastructures, ultimately accelerating development cycles and improving overall product quality. +`, + icon: Package, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/terraform-provider.png', + heroImageLight: '/images/features/terraform-provider-light.png', + docsUrl: 'https://supabase.com/docs/guides/deployment/terraform', + slug: 'terraform-provider', + }, + { + title: 'Read replicas', + subtitle: 'Deploy read-only databases across multiple regions for lower latency.', + description: ` +Supabase Read Replicas allow you to deploy additional read-only databases that are kept in sync with your Primary database. This feature enhances performance, improves resource management, and reduces latency for global applications. + +## Key features +1. Load balancing: Distribute read operations across multiple databases, reducing load on the Primary. +2. Global deployment: Deploy replicas closer to users for reduced latency. +3. Dedicated endpoints: Each replica has its own database and API endpoints. +4. API load balancer: Automatically balance GET requests across all available endpoints. +5. Centralized configuration: Settings are propagated across all databases in a project. +6. Monitoring tools: Track replication lag and resource utilization through the Supabase Dashboard. + +## Benefits: +- Improved performance: Serve data from the nearest location, reducing response times. +- Increased availability: Distribute read traffic across multiple replicas for enhanced resilience. +- Scalability: Handle higher read loads by offloading queries to replicas. +- Data redundancy: Replicas provide additional copies of your data. +- Analytics support: Run resource-intensive queries on replicas without impacting production. + +## Read Replicas are particularly valuable for: +- Global applications serving users across different regions +- High-traffic websites with read-heavy workloads +- Real-time analytics dashboards requiring low-latency data access +- Applications needing to scale read capacity independently of write capacity + +By leveraging Read Replicas, you can achieve consistent low-latency performance globally, improve application responsiveness for read operations, and enhance system reliability through better resource distribution and redundancy. +`, + icon: Database, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: 'https://www.youtube-nocookie.com/embed/PX3R1fXjJ2M', + docsUrl: 'https://supabase.com/docs/guides/platform/read-replicas', + slug: 'read-replicas', + }, + { + title: 'Fly Postgres', + subtitle: 'Launch the Supabase stack on Fly.io edge network.', + description: ` +Fly Postgres allows you to deploy Supabase databases on the Fly.io edge network, bringing your data closer to users and reducing latency for global applications. + +## Key features +1. Edge deployment: Launch databases in any region where Fly.io operates. +2. CLI integration: Set up and manage databases using the Fly CLI. +3. Supabase dashboard access: Use familiar Supabase tools for database management. +4. IPv6 support: Connect directly to databases using IPv6. +5. Supavisor support: Use Supavisor for IPv4 connections when needed. +6. Automatic Supabase organization creation: Seamless integration with Fly accounts. + +## Benefits: +- Improved global performance: Serve data from edge locations, minimizing latency. +- Simplified deployment: Easily set up databases using Fly CLI commands. +- Familiar tooling: Access Supabase features like SQL editor and table editor. +- Flexible connectivity: Support for both IPv6 and IPv4 (via Supavisor) connections. +- Integrated management: Manage database resources alongside other Fly applications. + +## Fly Postgres is particularly valuable for: +- Global applications requiring low-latency database access worldwide +- Developers familiar with Fly.io looking to integrate Supabase databases +- Projects needing easy deployment and management of edge-located databases + +By leveraging Fly Postgres, you can achieve improved global performance for your applications while maintaining the ease of use and powerful features of Supabase. +`, + icon: Cloud, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/platform/fly-postgres', + slug: 'fly-postgres', + }, + { + title: 'Postgres Extensions', + subtitle: 'Enhance your database with popular Postgres extensions.', + description: ` +Supabase supports a wide array of Postgres extensions, allowing you to enhance your database with additional functionalities. These extensions enable you to tailor your database to specific needs, from geospatial data to full-text search and advanced analytics. + +## Key features +1. Pre-installed extensions: Supabase comes with over 50 pre-configured extensions. +2. Easy management: Enable or disable extensions via the dashboard or SQL commands. +3. Flexible installation: Install custom SQL extensions using Supabase's SQL editor. +4. Automatic upgrades: Access new extension versions through infrastructure upgrades. +5. Schema control: Most extensions are installed under the extensions schema. + +## Benefits: +- Enhanced functionality: Add specialized features to your database without changing your application architecture. +- Optimized performance: Leverage extensions for efficient data processing and querying. +- Flexibility: Choose from a vast ecosystem of extensions to meet your project requirements. +- Easy integration: Incorporate powerful features into your existing database structure. +- Cost-effective solutions: Implement complex features without separate, specialized databases. + +Popular Postgres extensions supported by Supabase include: +- PostGIS: For working with geospatial data +- pgvector: Enables efficient similarity search and machine learning operations +- pgcrypto: Provides cryptographic functions +- pgjwt: Allows for JSON Web Token (JWT) generation and verification +- pg_net: Enables making HTTP requests from the database +- pgroonga: Provides full-text search capabilities for various languages +- timescaledb: Optimizes the database for time-series data + +Postgres extensions are valuable for a wide range of applications, from GIS and machine learning projects to security-focused applications and IoT systems dealing with time-series data. + +By leveraging these extensions, you can implement complex features more easily, reduce the need for external services, and tailor your database to specific project requirements. +`, + icon: Puzzle, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/postgres-extensions.png', + heroImageLight: '/images/features/postgres-extensions-light.png', + docsUrl: 'https://supabase.com/docs/guides/database/extensions', + slug: 'postgres-extensions', + }, + { + title: 'Database Webhooks', + subtitle: 'Trigger external payloads on database events.', + description: ` +Database Webhooks allow you to send real-time data from your database to another system whenever a table event occurs. You can hook into three table events: INSERT, UPDATE, and DELETE, with all events fired after a database row is changed. This feature provides a convenient way to integrate your Supabase database with external applications and services. + +## Key benefits +1. Real-Time Data Transfer: Automatically send data to external systems in response to database changes, ensuring timely updates. +2. Flexibility: Configure webhooks for specific tables and events, allowing for tailored integrations based on your application's needs. +3. Asynchronous Processing: Built on the pg_net extension, webhooks operate asynchronously, preventing long-running network requests from blocking database operations. +4. Easy Setup: Create webhooks directly from the Supabase Dashboard or through SQL statements, making integration straightforward. +5. Payload Customization: Automatically generated payloads provide relevant data about the event, including the new and old record states. + +This feature is particularly useful for developers looking to automate workflows and integrate their databases with third-party services like payment processors or notification systems. +`, + icon: Cloud, + products: [PRODUCT_SHORTNAMES.DATABASE], + heroImage: '/images/features/database-webhooks.png', + heroImageLight: '/images/features/database-webhooks-light.png', + docsUrl: 'https://supabase.com/docs/guides/database/webhooks', + slug: 'database-webhooks', + }, + { + title: 'Vault', + subtitle: 'Manage secrets safely in Postgres.', + description: ` +Vault is a Postgres extension and accompanying Supabase UI that simplifies the secure storage of encrypted secrets and other sensitive data in your database. This feature allows developers to utilize Postgres in innovative ways beyond its standard capabilities. + +## Key benefits: +1. Secure Storage: Secrets are stored on disk in an encrypted format, ensuring they remain protected even in backups or replication streams. +2. Easy Management: The Supabase dashboard UI makes it simple to store and manage secrets, including environment variables and API keys. +3. Flexible Encryption: Users can create custom encryption keys for different purposes, enhancing data security. +4. Seamless Access: Secrets can be accessed from SQL as easily as querying a table, facilitating their use in Postgres Functions, Triggers, and Webhooks. +5. Robust Security Features: The Vault employs authenticated encryption to ensure that secrets cannot be forged or decrypted without proper authorization. + +This feature is particularly useful for teams looking to enhance their security posture by managing sensitive data directly within their database environment. +`, + icon: Lock, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: 'https://supabase.com/docs/img/guides/database/vault-hello-compressed.mp4', + docsUrl: 'https://supabase.com/docs/guides/database/vault', + slug: 'vault', + }, + { + title: 'Supavisor', + subtitle: 'A scalable connection pooler for Postgres.', + description: ` +Supavisor is a cloud-native connection pooler designed for Postgres, built to handle millions of connections efficiently. It was developed in Elixir and integrates advanced features such as query load balancing, named prepared statement support, and query cancellation. Supavisor enhances the performance of Postgres by managing connections effectively, ensuring that applications can scale seamlessly under heavy loads. + +## Key benefits: +1. Scalable Architecture: Supports high concurrency and rapid I/O, making it suitable for applications with significant connection demands. +2. Query Load Balancing: Automatically distributes read requests across primary and replica servers to optimize performance. +3. Named Prepared Statements: Allows clients to reuse query plans across connections, improving efficiency and reducing overhead. +4. Query Cancellation: Provides the ability to cancel long-running queries easily, enhancing user experience during database interactions. +5. Easy Integration: New Supabase projects come with a Supavisor connection string by default, simplifying setup for developers. + +This feature is particularly valuable for teams looking to optimize their database interactions and ensure robust performance as their applications scale. +`, + icon: Zap, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: 'https://www.youtube-nocookie.com/embed/ogYNmJOFEpk', + docsUrl: 'https://supabase.com/blog/supavisor-postgres-connection-pooler', + slug: 'supavisor', + }, + // Realtime + { + title: 'Realtime Postgres changes', + subtitle: 'Receive your database changes through websockets.', + description: ` +Supabase's Realtime Postgres Changes feature allows you to listen to database changes in real-time using the Realtime system. This capability enables you to build responsive, live-updating applications that reflect database changes instantly. + +## Key features +1. Event-based listening: Subscribe to INSERT, UPDATE, DELETE, or all (*) events. +2. Schema and table targeting: Listen to changes in specific schemas or tables. +3. Granular filtering: Apply filters to receive only relevant changes. +4. Multiple subscriptions: Listen to different combinations of events, schemas, and tables in a single channel. +5. Row-level security integration: Respect database permissions when broadcasting changes. + +## Benefits: +- Real-time updates: Receive instant notifications when data changes, enabling live-updating UIs. +- Efficient data syncing: Keep client-side data in sync with the database without constant API calls. +- Flexible subscriptions: Tailor your subscriptions to specific events, schemas, tables, or conditions. +- Improved user experience: Provide users with up-to-date information without page refreshes. +- Simplified architecture: Implement real-time features without separate messaging systems. + +## Postgres Changes are valuable for: +- Collaborative applications where multiple users work on shared data +- Real-time dashboards and analytics platforms +- Live chat and messaging systems +- Applications requiring instant updates based on database changes + +Supabase's Realtime Postgres Changes feature provides a powerful tool for creating responsive, real-time applications while leveraging the full capabilities of your Postgres database. +`, + icon: DatabaseZap, + products: [PRODUCT_SHORTNAMES.REALTIME], + heroImage: 'https://www.youtube-nocookie.com/embed/2rUjcmgZDwQ', + docsUrl: 'https://supabase.com/docs/guides/realtime/postgres-changes', + slug: 'realtime-postgres-changes', + }, + { + title: 'Realtime Broadcast', + subtitle: 'Send messages between connected users through websockets.', + description: ` +Supabase's Realtime Broadcast feature enables real-time communication between connected clients using the Realtime system. This functionality allows you to build interactive, collaborative applications where users can instantly share messages or data in real-time. + +## Key features +1. Channel-based messaging: Send and receive messages within specific channels or rooms. +2. Event filtering: Subscribe to specific event types for targeted message handling. +3. Flexible payload: Transmit various types of data in the message payload. +4. Self-send option: Configure channels to receive messages sent by the sender. +5. Message acknowledgment: Confirm message receipt by the Realtime server. +6. REST API support: Send broadcast messages without an established WebSocket connection. + +## Benefits: +- Instant communication: Enable real-time messaging and data sharing between users. +- Low latency: Achieve near-instantaneous data transmission for time-sensitive applications. +- Flexible implementation: Use WebSocket connections or REST API calls to send messages. +- Simplified architecture: Build real-time features without managing complex messaging infrastructure. +- Enhanced user engagement: Create interactive, collaborative experiences that keep users connected. + +## Realtime Broadcast is particularly valuable for: +- Chat applications and messaging platforms +- Collaborative tools like shared documents or whiteboards +- Multiplayer games requiring real-time state synchronization +- Live auction or bidding systems +- Real-time commenting systems for blogs or social media platforms +- Interactive presentations or educational platforms + +Supabase's Realtime Broadcast feature provides a powerful tool for creating responsive, real-time applications with minimal setup and infrastructure management. +`, + icon: MessageCircle, + products: [PRODUCT_SHORTNAMES.REALTIME], + heroImage: 'https://www.youtube-nocookie.com/embed/BelYEMJ2N00', + docsUrl: 'https://supabase.com/docs/guides/realtime/broadcast', + slug: 'realtime-broadcast', + }, + { + title: 'Realtime Presence', + subtitle: 'Synchronize shared state between users through websockets.', + description: ` +Supabase's Realtime Presence feature allows you to track and synchronize shared state between connected users in real-time. This capability enables you to build collaborative applications where users can see each other's status, actions, or any custom state information instantly. + +## Key features +1. Real-time state synchronization: Track and update shared state across multiple clients. +2. Event-based tracking: Listen to 'sync', 'join', and 'leave' events for state changes. +3. Custom state definition: Share any type of state information between users. +4. Unique client identification: Use auto-generated or custom keys to identify clients. +5. Channel-based organization: Group users and their states into specific channels or rooms. +6. Easy state management: Track, untrack, and update state with simple method calls. + +## Benefits: +- Real-time user tracking: Monitor which users are currently active or online. +- Enhanced interactivity: Create more engaging, collaborative user experiences. +- Reduced complexity: Implement shared state features without building custom synchronization logic. +- Flexible use cases: Adapt the feature for various applications, from simple online indicators to complex collaborative tools. + +## Realtime Presence is particularly valuable for: +- Collaborative document editing platforms +- Multiplayer games showing player positions or status +- Team collaboration tools with user availability indicators +- Live streaming platforms displaying viewer counts and engagement +- Project management applications with real-time task assignments +- Social media platforms showing active users in a group or chat + +Supabase's Realtime Presence feature provides a powerful tool for creating interactive, real-time applications that require shared state management, allowing developers to focus on creating unique and valuable user experiences. +`, + icon: Users, + products: [PRODUCT_SHORTNAMES.REALTIME], + heroImage: 'https://www.youtube-nocookie.com/embed/BelYEMJ2N00', + docsUrl: 'https://supabase.com/docs/guides/realtime/presence', + slug: 'realtime-presence', + }, + // Auth + { + title: 'Email login', + subtitle: 'Build email logins for your application or website.', + description: ` +Supabase's Email Login feature enables secure email-based authentication for your applications, allowing users to create accounts and sign in using their email addresses. + +## Key features +1. Email signup and signin: Implement user registration and login with email and password. +2. Email verification: Option to require email confirmation before account activation. +3. Password reset flow: Built-in functionality for secure password resets. +4. Customizable redirect URLs: Specify where users are directed after email confirmation. +5. Flexible implementation: Support for both implicit and PKCE authentication flows. +6. SMTP integration: Use Supabase's email service or configure a custom SMTP server. + +## Benefits: +- Secure authentication: Implement industry-standard security practices. +- Customizable workflows: Tailor the signup and login processes to your needs. +- Seamless integration: Works with Supabase's other auth providers and features. +- Local development support: Test email flows using built-in tools like Inbucket. + +## Email login is valuable for: +- SaaS applications requiring user accounts +- E-commerce platforms with customer profiles +- Community websites and forums +- Enterprise applications with employee logins + +Supabase's Email Login provides a robust foundation for user authentication, allowing you to focus on building your core application features while ensuring secure account management. +`, + icon: Mail, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '/images/features/email-login.png', + heroImageLight: '/images/features/email-login-light.png', + docsUrl: 'https://supabase.com/docs/guides/auth/passwords', + slug: 'email-login', + }, + { + title: 'Social login', + subtitle: 'Provide social logins from platforms like Apple, GitHub, and Slack.', + description: ` +Supabase's Social Login feature enables users to authenticate using their existing accounts from popular social platforms and services, providing a streamlined, password-free access to your application. + +## Key features +1. Multiple provider support: Integrate with various platforms like Google, Facebook, Twitter, and more. +2. OAuth standard: Implement secure authentication using the open OAuth standard. +3. Provider tokens: Access provider tokens for additional API calls to the OAuth provider. +4. Customizable setup: Configure each social provider according to your needs. +5. User data access: Retrieve additional user information (with permission) from social providers. + +## Benefits: +- Simplified onboarding: Reduce friction in the signup process, potentially increasing conversion rates. +- Improved user experience: Offer quick, familiar login options to users. +- Enhanced security: Leverage the robust security measures of established platforms. +- Rich user profiles: Access additional user data to personalize experiences. + +## Social login is valuable for: +- Mobile apps seeking to minimize user input +- E-commerce platforms aiming to reduce cart abandonment +- Content platforms looking to personalize user experiences +- Community-driven websites fostering user engagement +- B2B applications integrating with professional networks + +Supabase's Social Login feature allows you to improve user experience and potentially increase signup rates while maintaining security and control over user authentication. +`, + icon: Users, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '/images/features/social-login.png', + heroImageLight: '/images/features/social-login-light.png', + docsUrl: 'https://supabase.com/docs/guides/auth/social-login', + slug: 'social-login', + }, + { + title: 'Phone logins', + subtitle: 'Provide phone logins using a third-party SMS provider.', + description: ` +Supabase's Phone Login feature allows users to authenticate using their phone numbers, providing a password-free login experience with SMS verification. + +## Key features +1. OTP-based authentication: Users sign in with a one-time code sent via SMS. +2. Phone number updates: Allow logged-in users to change their associated phone number. +3. Customizable settings: Configure rate limits and OTP expiration times. +4. Multiple SMS provider support: Integrate with various SMS services like MessageBird, Twilio, and Vonage. +5. Native Mobile Login support: Utilize built-in identity providers for Android and iOS. + +## Benefits: +- Simplified authentication: Users can sign up and log in using just their phone number. +- Enhanced security: Implement two-factor authentication (2FA) using SMS verification codes. +- Reduced friction: Eliminate the need for users to remember passwords. +- Verified user base: Ensure each account is associated with a unique, verifiable phone number. + +## Phone login is valuable for: +- Mobile apps prioritizing quick and easy onboarding +- Ride-sharing or delivery services requiring verified contact information +- Messaging platforms where phone numbers serve as user identifiers +- Applications targeting regions where phone usage exceeds email usage + +Supabase's Phone Login feature enables a seamless authentication experience, particularly suited for mobile and SMS-centric applications, while providing an additional layer of security through phone number verification. +`, + icon: Smartphone, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '/images/features/phone-login.png', + heroImageLight: '/images/features/phone-login-light.png', + docsUrl: 'https://supabase.com/docs/guides/auth/phone-login', + slug: 'phone-logins', + }, + { + title: 'Passwordless login via Magic Links', + subtitle: 'Build passwordless logins via magic links for your application or website.', + description: ` +Supabase's Passwordless Login via Magic Links allows users to sign in by clicking a unique link sent to their email address, eliminating the need for traditional passwords. + +## Key features +1. One-time use links: Each Magic Link is valid for a single login attempt. +2. Automatic user creation: New users can be automatically signed up (optional). +3. Customizable email templates: Modify the Magic Link email content to fit your brand. +4. Configurable settings: Adjust request limits and link expiration times. +5. Redirect URL control: Specify allowed redirect destinations after login. +6. PKCE flow support: Enhanced security for server-side rendering. + +## Benefits: +- Enhanced security: Reduce risks associated with password-based vulnerabilities. +- Improved user experience: Remove the friction of remembering and entering passwords. +- Reduced support overhead: Decrease password-related support requests. +- Simplified onboarding: Lower the barrier to entry for new users. + +## Passwordless login via Magic Links is valuable for: +- SaaS platforms prioritizing security and user experience +- Financial applications requiring strong authentication +- E-commerce sites aiming to reduce login friction +- Enterprise applications managing access for numerous employees +- Any application targeting users fatigued by managing multiple passwords + +Supabase's Magic Links feature offers a secure and user-friendly authentication method, aligning with modern security practices while providing a frictionless login experience. +`, + icon: Lock, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/auth/auth-email-passwordless', + slug: 'passwordless-login-via-magicklink', + }, + { + title: 'Multi-Factor Authentication (MFA)', + subtitle: 'Add an extra layer of security to your application with MFA.', + description: ` +Supabase's Multi-Factor Authentication (MFA) adds an extra layer of security to your application by verifying user identity through additional steps beyond the initial login. + +## Key features +1. Multiple authentication methods: Support for App Authenticator (TOTP) and phone messaging. +2. Flexible enrollment: API for building custom MFA setup interfaces. +3. Challenge and Verify APIs: Securely validate user access to additional factors. +4. Authenticator Assurance Levels: JWT claims (aal1, aal2) to indicate the level of identity verification. +5. Customizable enforcement: Options to enforce MFA for all users, new users only, or opt-in basis. +6. Database integration: Row Level Security policies to enforce MFA rules at the database level. + +## Benefits: +- Enhanced security: Significantly reduce the risk of unauthorized account access. +- User trust: Demonstrate a commitment to protecting user data and privacy. +- Compliance support: Meet security requirements for regulated industries. +- Flexible implementation: Customize MFA flows to suit your application's needs. + +## MFA is particularly valuable for: +- Financial services and banking applications +- Healthcare platforms managing protected health information +- Enterprise applications with access to confidential business data +- E-commerce sites protecting user payment information +- Any application storing sensitive user data + +Supabase's MFA feature provides a robust tool for enhancing application security, allowing developers to implement strong authentication measures tailored to their specific requirements. +`, + icon: Lock, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/auth/auth-mfa', + slug: 'multi-factor-authentication', + }, + { + title: 'Authorization via Row Level Security', + subtitle: 'Control the data each user can access with Postgres Policies.', + description: ` +Supabase's Row Level Security (RLS) feature allows you to implement granular authorization rules directly in your Postgres database, securing your data at the row level. + +## Key features +1. Policy-based access control: Create SQL rules to determine data access for each table. +2. Role-specific policies: Define different access rules for authenticated and anonymous users. +3. Flexible policy types: Implement SELECT, INSERT, UPDATE, and DELETE policies. +4. Integration with Supabase Auth: Use built-in helper functions like auth.uid() and auth.jwt() in policies. +5. Performance optimization: Utilize indexes and optimized query patterns for efficient policy execution. +6. Bypass options: Use service keys or create roles with bypassrls privilege for administrative tasks. + +## Benefits: +- Enhanced data security: Control access to individual rows based on user attributes or roles. +- Simplified application logic: Reduce complex authorization checks in your application code. +- Consistency across clients: Ensure uniform access control regardless of data access method. +- Centralized policy management: Define and manage access rules directly in the database. + +## RLS is particularly valuable for: +- Multi-tenant applications requiring data isolation +- Healthcare systems needing patient data privacy +- Financial platforms with strict data access controls +- Collaborative tools where users should only see their own or shared data +- Any application dealing with sensitive or personalized data + +Supabase's RLS feature provides a powerful tool for implementing sophisticated access control patterns with minimal application code, enhancing security while simplifying development. +`, + icon: Lock, + products: [PRODUCT_SHORTNAMES.DATABASE, PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: `https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/website/supabase-rls.mp4`, + docsUrl: 'https://supabase.com/docs/guides/auth/row-level-security', + slug: 'row-level-security', + }, + { + title: 'Captcha protection', + subtitle: 'Add Captcha to your sign-in, sign-up, and password reset forms.', + description: ` +Supabase's Captcha Protection feature allows you to integrate CAPTCHA challenges into your authentication flows, providing an additional layer of security against automated attacks and bot activities. + +## Key features +1. Multiple provider support: Integration with hCaptcha and Cloudflare Turnstile. +2. Easy configuration: Enable and configure CAPTCHA through the Supabase Dashboard. +3. Flexible implementation: Support for various JavaScript frameworks and custom frontend integration. +4. Auth flow integration: Seamlessly add CAPTCHA to sign-up, sign-in, and password reset processes. +5. Token-based verification: Use CAPTCHA tokens in Supabase auth functions for enhanced security. + +## Benefits: +- Bot protection: Defend against automated attacks on authentication endpoints. +- Reduced spam: Minimize fake account creation and spam submissions. +- Customizable security: Adjust CAPTCHA difficulty based on risk assessment. +- Improved security posture: Demonstrate commitment to security best practices. + +## Captcha protection is valuable for: +- High-traffic websites vulnerable to automated attacks +- E-commerce platforms preventing fake account creation +- Forums and community sites reducing spam registrations +- Financial services applications adding an extra layer of security +- Any public-facing forms susceptible to bot submissions + +Supabase's Captcha Protection feature provides a powerful tool for enhancing application security, helping to protect against various automated threats while maintaining a user-friendly experience. +`, + icon: RectangleEllipsis, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '/images/features/auth-captcha-protection.png', + heroImageLight: '/images/features/auth-captcha-protection-light.png', + docsUrl: 'https://supabase.com/docs/guides/auth/auth-captcha', + slug: 'auth-captcha-protection', + }, + { + title: 'Server-side Auth', + subtitle: 'Helpers for implementing user authentication in popular server-side languages.', + description: ` +Supabase's Server-side Auth feature provides tools and utilities for implementing secure user authentication in server-side environments, complementing client-side auth implementations. + +## Key features +1. SSR compatibility: Fully supports Server-Side Rendering frameworks. +2. Cookie-based sessions: Store user sessions in cookies instead of local storage for enhanced security. +3. PKCE flow support: Implement the more secure PKCE authentication flow for server-side apps. +4. @supabase/ssr package: Simplified setup for Supabase client in SSR environments (currently in beta). +5. Framework integration: Easily integrate with popular SSR frameworks like Next.js and SvelteKit. + +## Benefits: +- Enhanced security: Implement secure token verification and session management on the server. +- Simplified development: Use pre-built helpers to handle common auth tasks, reducing boilerplate code. +- Consistent auth experience: Maintain a unified auth approach across client and server components. +- SSR support: Enable authenticated server-side rendering for improved performance and SEO. + +## Server-side Auth is valuable for: +- Server-rendered web applications requiring authenticated content +- APIs and microservices needing to verify client authenticity +- Hybrid apps combining client and server-side rendering +- Projects leveraging SSR frameworks like Next.js, Nuxt, or SvelteKit +- Applications requiring secure, programmatic access to user session data + +Supabase's Server-side Auth feature allows developers to create more secure and robust applications with consistent authentication across both client and server components, adhering to security best practices while leveraging Supabase's auth system. +`, + icon: Server, + products: [PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/auth/server-side', + slug: 'server-side-auth', + }, + // Storage + { + title: 'File storage', + subtitle: 'Supabase Storage makes it simple to store and serve files.', + description: ` +Supabase Storage provides a scalable solution for storing and serving files in your applications, allowing easy management of user-generated content and assets within your Supabase project. + +## Key features +1. Global CDN: Serve files quickly from over 285 cities worldwide. +2. Image optimization: Resize and compress media files on the fly. +3. Flexible file types: Store images, videos, documents, and any other file type. +4. Access control: Implement fine-grained access using Postgres RLS policies. +5. Resumable uploads: Support for protocols like TUS for reliable file transfers. +6. Organization: Structure files into buckets and folders for efficient management. + +## Benefits: +- Seamless integration: Use the same Supabase client for both database and storage operations. +- Scalability: Handle large numbers of files and high traffic loads without infrastructure management. +- Performance: Quick file serving with global CDN support. +- Cost-effectiveness: Pay only for the storage you use, with no upfront costs. + +## File storage is valuable for: +- Social media platforms storing user-uploaded media +- E-commerce sites managing product images and documents +- Content management systems handling various file types +- Collaborative tools storing shared assets +- Mobile apps needing to sync user data and media + +Supabase Storage simplifies adding robust file management to your applications, allowing you to focus on building unique features while relying on Supabase for secure, scalable file storage and serving. +`, + icon: Folders, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: '/images/features/file-storage.png', + heroImageLight: '/images/features/file-storage-light.png', + docsUrl: 'https://supabase.com/docs/guides/storage', + slug: 'file-storage', + }, + { + title: 'Content Delivery Network', + subtitle: 'Cache large files using the Supabase CDN.', + description: ` +Supabase's Content Delivery Network (CDN) feature caches and serves assets uploaded to Supabase Storage efficiently across the globe, improving latency for users worldwide. + +## Key features +1. Global distribution: Serve content from geographically distributed servers. +2. Origin caching: Cache content from the storage server in your project's region. +3. Intelligent routing: Direct users to the nearest CDN node for faster access. +4. Cache status tracking: Monitor cache hits and misses via the cf-cache-status header. +5. Public and private bucket support: Optimize caching based on bucket privacy settings. + +## Benefits: +- Improved performance: Reduce latency by serving content from servers closest to the user. +- Scalability: Handle high traffic loads without straining your main servers. +- Global reach: Deliver content efficiently to users worldwide without managing global infrastructure. +- Enhanced security: Additional layer of protection against DDoS and other application attacks. +- Improved availability: Increase content redundancy through distributed serving. + +## The CDN feature is valuable for: +- Media-heavy websites serving large images or video files +- Global applications requiring fast content delivery across different regions +- E-commerce platforms needing to serve product images quickly +- Content publishers distributing large documents or multimedia files +- Any application dealing with large file downloads or streaming + +Supabase's CDN significantly improves the performance and user experience of your applications, especially for users accessing large files or media content globally, without the complexity of managing your own CDN infrastructure. +`, + icon: Cloud, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/storage/cdn', + slug: 'cdn', + }, + { + title: 'Smart Content Delivery Network', + subtitle: 'Automatically revalidate assets at the edge via the Smart CDN.', + description: ` +Supabase's Smart CDN automatically synchronizes asset metadata to the edge, ensuring content is quickly delivered and remains up-to-date without manual intervention. + +## Key features +1. Automatic cache revalidation: Invalidates CDN cache when assets are changed or deleted. +2. Improved cache hit rate: Shields origin server from unchanged asset requests, even with different query strings. +3. Long-term edge caching: Stores assets on the CDN for extended periods while maintaining freshness. +4. Flexible browser caching: Control browser cache duration using the cacheControl option. +5. Rapid propagation: Changes reflect across global data centers within 60 seconds. +6. Cache bypass option: Use unique query strings to fetch directly from the origin when needed. + +## Benefits: +- Content freshness: Users always receive the most recent version of assets. +- Reduced origin load: Minimize requests to the origin server by optimizing edge caching. +- Improved user experience: Deliver fast-loading, up-to-date content globally. +- Cost optimization: Reduce bandwidth costs by serving more content from the edge. + +## The Smart CDN feature is valuable for: +- Dynamic websites with frequently updated content +- E-commerce platforms with real-time inventory updates +- News and media sites delivering the latest information +- Applications with rapidly changing user-generated content +- Global platforms requiring both speed and content accuracy + +Supabase's Smart CDN optimizes both performance and content accuracy, providing a superior user experience for applications with dynamic or frequently updated content, without the complexity of manual cache management. +`, + icon: Cloud, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: 'https://www.youtube-nocookie.com/embed/NpEl20iuOtg', + docsUrl: 'https://supabase.com/docs/guides/storage/cdn/smart-cdn', + slug: 'smart-cdn', + }, + { + title: 'Image transformations', + subtitle: 'Transform images on the fly.', + description: ` +Supabase's Image Transformations feature allows you to optimize and resize images on-the-fly, directly from your storage buckets. + +## Key features +1. Dynamic resizing: Adjust image dimensions using width and height parameters. +2. Quality control: Set image quality with a scale of 20 to 100. +3. Resize modes: Choose from 'cover', 'contain', or 'fill' to suit your needs. +4. Automatic format optimization: Convert images to WebP for supported browsers. +5. Flexible implementation: Use with public URLs, signed URLs, or direct downloads. +6. Next.js integration: Custom loader for optimized images in Next.js applications. +7. Self-hosting option: Deploy your own image transformation service using Imgproxy. + +## Benefits: +- Performance optimization: Reduce bandwidth usage and improve load times with optimized images. +- Storage efficiency: Store a single high-quality version and generate variants as needed. +- Responsive design support: Serve appropriately sized images for different devices and layouts. +- Simplified workflow: Eliminate the need for manual image processing and multiple version storage. + +## Image transformations are valuable for: +- Responsive web applications requiring different image sizes +- E-commerce platforms showcasing product images +- Content management systems adapting images for various layouts +- Mobile apps optimizing images for cellular networks +- Any application handling large volumes of images in varying contexts + +Supabase's Image Transformations feature enables you to efficiently manage and serve optimized images, improving your application's performance and user experience while saving time and resources. +`, + icon: Image, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: 'https://www.youtube-nocookie.com/embed/dLqSmxX3r7I', + docsUrl: 'https://supabase.com/docs/guides/storage/image-transformations', + slug: 'image-transformations', + }, + { + title: 'Resumable uploads', + subtitle: 'Upload large files using resumable uploads.', + description: ` +Supabase's Resumable Uploads feature enables reliable transfer of large files, allowing uploads to be paused and resumed, particularly useful for files exceeding 6MB or in unstable network conditions. + +## Key features +1. TUS protocol implementation: Utilizes the open protocol for resumable uploads. +2. Progress tracking: Provides upload progress events for better user feedback. +3. Chunk-based uploads: Uses 6MB chunks for efficient transfer and resumption. +4. Retry mechanism: Implements automatic retries with customizable delay intervals. +5. Metadata support: Allows setting of bucket name, object name, content type, and cache control. +6. Upsert option: Provides the ability to overwrite existing files if needed. +7. Flexible integration: Compatible with tus-js-client and other TUS-supporting libraries like Uppy. + +## Benefits: +- Reliability: Successfully upload large files even with unstable internet connections. +- User experience: Allow users to pause and resume uploads at their convenience. +- Bandwidth efficiency: Avoid re-uploading already transferred parts of a file after an interruption. +- Large file support: Confidently handle uploads of very large files without worrying about timeouts. + +## Resumable uploads are valuable for: +- Cloud storage applications handling large file transfers +- Video sharing platforms dealing with high-quality video uploads +- Backup and sync services ensuring data integrity during transfers +- Content creation tools handling large media files +- Any application where users need to upload sizeable files in potentially unstable conditions + +Supabase's Resumable Uploads feature significantly improves the reliability and user experience of file uploads, especially for large files or in scenarios with unpredictable network conditions. +`, + icon: UploadCloud, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: 'https://www.youtube-nocookie.com/embed/pT2PcZFq_M0', + docsUrl: 'https://supabase.com/docs/guides/storage/uploads/resumable-uploads', + slug: 'resumable-uploads', + }, + { + title: 'S3 compatibility', + subtitle: 'Interact with Storage from tools which support the S3 protocol.', + description: ` +Supabase Storage offers compatibility with the S3 protocol, allowing you to use any S3 client to interact with your Storage objects. + +## Key features +1. Protocol interoperability: Support for standard, resumable, and S3 uploads, all interoperable. +2. Extensive API support: Implementation of most commonly used S3 endpoints for bucket and object operations. +3. Bucket management: Support for creating, deleting, and listing buckets. +4. Object handling: Capabilities for uploading, downloading, copying, and deleting objects. +5. Multipart uploads: Support for creating, completing, and aborting multipart uploads. +6. Metadata management: Ability to set and retrieve system metadata for objects. +7. Conditional operations: Support for conditional GET and HEAD requests. + +## Benefits: +- Ecosystem compatibility: Leverage existing S3-compatible tools and libraries with Supabase Storage. +- Familiar workflows: Use well-known S3 commands and SDKs for storage operations. +- Flexibility: Choose the most suitable client libraries or tools for your specific needs. +- Easy migration: Simplify the process of moving from S3 to Supabase or using both in parallel. + +## S3 compatibility is valuable for: +- DevOps teams using S3-based deployment and backup scripts +- Data analysis workflows leveraging S3-compatible data lakes +- Content management systems with existing S3 integrations +- Legacy applications built around S3 storage +- Any project looking to leverage the vast ecosystem of S3-compatible tools + +Supabase's S3 compatibility allows seamless integration with existing workflows and tools, providing flexibility in data storage and management while benefiting from Supabase's managed solution. +`, + icon: UploadCloud, + products: [PRODUCT_SHORTNAMES.STORAGE], + heroImage: '/images/features/s3-compatibility.png', + heroImageLight: '/images/features/s3-compatibility-light.png', + docsUrl: 'https://supabase.com/docs/guides/storage/s3/compatibility', + slug: 's3-compatibility', + }, + // Functions + { + title: 'Deno Edge Functions', + subtitle: 'Globally distributed TypeScript functions to execute custom business logic.', + description: ` +Supabase's Deno Edge Functions allow you to deploy and run custom TypeScript functions globally at the edge, bringing your code closer to users for enhanced performance and reduced latency. + +## Key features +1. Global distribution: Functions run close to users, reducing latency. +2. TypeScript support: Leverage TypeScript's type safety and modern language features. +3. Deno runtime: Benefit from Deno's security-first approach and modern JavaScript APIs. +4. Seamless integration: Easy integration with other Supabase services like Auth and Database. +5. Versatile use cases: Support for webhooks, third-party integrations, and custom API endpoints. +6. Open-source and portable: Run locally or on any Deno-compatible platform. +7. Rich ecosystem: Wide range of examples and integrations available. + +## Benefits: +- Low-latency responses: Ideal for globally distributed applications. +- Scalability: Automatically handle varying loads without manual intervention. +- Flexibility: Extend Supabase project capabilities beyond database operations. +- Developer-friendly: Fast deployments and rollbacks for agile development. + +## Deno Edge Functions are valuable for: +- API backends requiring low-latency responses globally +- Serverless applications needing custom logic close to users +- Webhook handlers for third-party service integrations +- Real-time data processing and transformation +- Custom authentication and authorization flows +- Integration with various services (e.g., Stripe, OpenAI, Hugging Face) +- Building bots for platforms like Discord and Telegram + +Supabase's Deno Edge Functions enable you to build responsive, globally distributed applications with custom server-side logic, supporting a wide range of use cases and integrations. +`, + icon: FileCode2, + products: [PRODUCT_SHORTNAMES.FUNCTIONS], + heroImage: 'https://www.youtube-nocookie.com/embed/5OWH9c4u68M', + docsUrl: 'https://supabase.com/docs/guides/functions', + slug: 'deno-edge-functions', + }, + { + title: 'Regional invocations', + subtitle: 'Execute an Edge Function in a region close to your database.', + description: ` +Supabase's Regional Invocations feature allows you to execute Edge Functions in a region close to your database, optimizing performance for database-intensive operations. This capability ensures low-latency communication between your functions and database, enhancing overall application responsiveness. + +## Key benefits +1. Reduced latency: Minimize round-trip time between functions and database for faster operations. +2. Improved performance: Enhance the speed of database queries and updates from your functions. +3. Data locality: Keep data processing close to where the data is stored for efficiency. +4. Consistency: Ensure that functions operate on the most up-to-date data with minimal delay. +5. Cost optimization: Reduce data transfer costs between regions. +6. Simplified architecture: Streamline your application's geographic setup for database operations. +7. Compliance support: Help meet data residency requirements by processing data in specific regions. + +## Regional invocations are particularly valuable for: +- Applications with data-intensive serverless functions +- Real-time data processing workflows requiring low latency +- Financial applications needing quick database reads and writes +- Analytics services processing large volumes of data +- Compliance-sensitive applications with data residency requirements +- Any scenario where minimizing function-to-database latency is crucial + +By leveraging Supabase's Regional Invocations, you can significantly enhance the performance of your database-centric serverless functions. This feature allows you to build more responsive applications, especially those requiring frequent or complex database interactions, by ensuring that your Edge Functions operate as close as possible to your Supabase database. +`, + icon: Globe, + products: [PRODUCT_SHORTNAMES.FUNCTIONS], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/functions/regional-invocation', + slug: 'regional-invocations', + }, + { + title: 'NPM compatibility', + subtitle: 'Edge functions natively support NPM modules and Node built-in APIs.', + description: ` +Supabase's NPM Compatibility feature for Edge Functions allows you to use NPM modules and Node.js built-in APIs directly in your Deno-based Edge Functions. This powerful capability bridges the gap between Deno and the vast Node.js ecosystem, giving you access to a wide range of libraries and familiar APIs. + +## Key benefits +1. Extensive library access: Leverage the vast NPM ecosystem in your Edge Functions. +2. Familiar Node.js APIs: Use built-in Node.js modules you're already familiar with. +3. Code reusability: Easily port existing Node.js code to Supabase Edge Functions. +4. Ecosystem compatibility: Integrate with services and tools designed for Node.js environments. +5. Simplified development: Reduce the learning curve for developers coming from a Node.js background. +6. Flexibility: Choose between Deno-native and Node-compatible libraries as needed. +7. Future-proofing: Adapt existing codebases to edge computing without major rewrites. + +## NPM compatibility is particularly valuable for: +- Projects migrating from Node.js-based serverless platforms to Supabase +- Developers looking to leverage specific NPM packages in edge computing scenarios +- Applications requiring Node.js-specific libraries not available in Deno +- Teams with existing Node.js codebases wanting to adopt edge computing +- Rapid prototyping using familiar Node.js modules and APIs +- Any project aiming to balance the benefits of Deno with Node.js ecosystem access + +By leveraging NPM Compatibility in Supabase Edge Functions, you can take advantage of the best of both worlds: the modern, secure runtime of Deno and the rich ecosystem of Node.js. This feature allows you to build powerful, efficient edge computing solutions while maintaining access to the tools and libraries you're familiar with, accelerating development and enabling more complex use cases at the edge. +`, + icon: FileCode2, + products: [PRODUCT_SHORTNAMES.FUNCTIONS], + heroImage: 'https://www.youtube-nocookie.com/embed/eCbiywoDORw', + docsUrl: 'https://supabase.com/blog/edge-functions-node-npm', + slug: 'npm-compatibility', + }, + // Vector + { + title: 'AI Integrations', + subtitle: 'Enhance applications with OpenAI and Hugging Face integrations.', + description: ` +Supabase's AI Integrations feature provides seamless connectivity with leading AI platforms like OpenAI and Hugging Face, allowing you to easily incorporate advanced AI capabilities into your applications. This powerful feature enables developers to leverage state-of-the-art machine learning models and natural language processing directly within their Supabase projects. + +## Key benefits +1. Easy AI integration: Incorporate advanced AI capabilities without managing complex infrastructure. +2. Versatile model access: Utilize a wide range of pre-trained models for various AI tasks. +3. Scalability: Handle AI workloads efficiently with Supabase's managed infrastructure. +4. Cost-effective AI deployment: Leverage powerful AI models without significant upfront investments. +5. Real-time AI processing: Combine AI capabilities with Supabase's real-time features for dynamic applications. +6. Data synergy: Seamlessly use your Supabase data with AI models for personalized experiences. +7. Rapid prototyping: Quickly experiment with and deploy AI-powered features in your applications. + +## AI Integrations are particularly valuable for: +- Natural language processing applications like chatbots and content analyzers +- Recommendation systems for e-commerce or content platforms +- Image and video analysis tools for media applications +- Predictive analytics dashboards for business intelligence +- Personalization engines for user experience enhancement +- Any application seeking to incorporate AI-driven insights or automation + +By leveraging Supabase's AI Integrations, you can rapidly develop and deploy sophisticated AI-powered applications. This feature bridges the gap between powerful AI models and your application data, enabling you to create more intelligent, responsive, and personalized user experiences without the complexity of managing separate AI infrastructure. +`, + icon: Brain, + products: [PRODUCT_SHORTNAMES.VECTOR], + heroImage: 'https://www.youtube-nocookie.com/embed/OgnYxRkxEUw', + docsUrl: 'https://supabase.com/docs/guides/ai/examples/huggingface-image-captioning', + slug: 'ai-integrations', + }, + // Platform + { + title: 'CLI', + subtitle: 'Use our CLI to develop your project locally and deploy.', + description: ` +Supabase's Command Line Interface (CLI) tool provides developers with a powerful and flexible way to manage their Supabase projects directly from the terminal. This feature streamlines the development workflow, allowing for local development, testing, and seamless deployment of Supabase projects. + +## Key benefits +1. Local development: Set up and run a local Supabase instance for development and testing. +2. Version control integration: Easily manage database migrations and schema changes with Git-like workflows. +3. Automated deployments: Deploy changes to your Supabase project with simple CLI commands. +4. Database migrations: Generate, apply, and revert database migrations effortlessly. +5. Environment management: Handle multiple environments (development, staging, production) efficiently. +6. Seed data management: Populate your database with test data for consistent development and testing. +7. CI/CD integration: Incorporate Supabase operations into your continuous integration and deployment pipelines. + +## The CLI is particularly valuable for: +- Development teams working on Supabase projects collaboratively +- DevOps professionals managing Supabase deployments across multiple environments +- Open-source projects requiring reproducible Supabase setups +- Developers preferring command-line tools for increased productivity +- Projects with complex database schemas requiring careful version control +- Any application needing a streamlined local-to-production workflow with Supabase + +By leveraging the Supabase CLI, you can significantly improve your development workflow, ensuring consistency between local and production environments, streamlining deployments, and making it easier to manage complex Supabase projects. This tool empowers developers to work more efficiently with Supabase, whether they're building small prototypes or large-scale applications. +`, + icon: Terminal, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: 'https://www.youtube-nocookie.com/embed/vyHyYpvjaks', + docsUrl: + 'https://supabase.com/docs/guides/local-development?queryGroups=package-manager&package-manager=pnpm', + slug: 'cli', + }, + { + title: 'Management API', + subtitle: 'Manage your projects programmatically.', + description: ` +Supabase's Management API provides a powerful interface for programmatically managing your Supabase projects. This feature allows developers and DevOps teams to automate project creation, configuration, and maintenance tasks, enabling more efficient and scalable project management workflows. + +## Key benefits +1. Automation: Automate repetitive tasks like project creation and configuration. +2. Scalability: Easily manage multiple Supabase projects across different environments. +3. CI/CD integration: Incorporate Supabase project management into your continuous integration and deployment pipelines. +4. Programmatic control: Manage projects dynamically based on your application's needs. +5. Consistent setup: Ensure uniform configuration across multiple projects or environments. +6. Audit trail: Keep track of project changes and configurations programmatically. +7. Resource optimization: Dynamically allocate and deallocate resources based on demand. + +## The Management API is particularly valuable for: +- Large organizations managing multiple Supabase projects +- SaaS platforms offering Supabase-powered backends to their customers +- DevOps teams implementing infrastructure-as-code practices +- Managed service providers offering Supabase as part of their stack +- Applications requiring dynamic project creation and management +- Any scenario where manual project management through the dashboard is impractical + +By utilizing the Supabase Management API, you can create more efficient, scalable, and automated workflows for managing your Supabase projects. This feature is especially powerful for organizations dealing with multiple projects or environments, enabling them to maintain consistency, reduce manual errors, and quickly respond to changing project requirements. +`, + icon: FileCode2, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '/images/features/management-api.png', + heroImageLight: '/images/features/management-api-light.png', + docsUrl: 'https://supabase.com/docs/guides/project-management/api', + slug: 'management-api', + }, + { + title: 'Role-Based Access Control (RBAC)', + icon: ShieldPlus, + subtitle: 'Define and manage user roles securely', + description: ` +Supabase's Role-Based Access Control (RBAC) feature provides a powerful and flexible way to manage user permissions within your application. RBAC allows you to define roles with specific sets of permissions and assign these roles to users, enabling fine-grained control over who can access what within your system. + +## Key benefits +1. Granular access control: Define precise permissions for different user types or job functions. +2. Simplified management: Easily manage permissions by assigning roles rather than individual permissions. +3. Scalability: Efficiently handle permissions for large numbers of users and resources. +4. Compliance support: Meet regulatory requirements for access control in various industries. +5. Reduced error risk: Minimize the chance of accidental permission assignments. +6. Auditing capabilities: Easily track and review role assignments and permission changes. +7. Flexibility: Quickly adapt to organizational changes by modifying role definitions. + +## RBAC is particularly valuable for: +- Enterprise applications with complex organizational structures +- Healthcare systems requiring strict data access controls +- Financial platforms with varying levels of user authority +- Content management systems with different contributor roles +- E-commerce platforms with layered admin permissions +- Any application needing to limit access to sensitive features or data + +By leveraging Role-Based Access Control, you can create a secure, scalable, and easily manageable access control system for your application. This feature allows you to implement complex permission structures with ease, ensuring that users only have access to the resources and functionalities appropriate for their role within the system. +`, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: 'https://www.youtube-nocookie.com/embed/kwoKmi6inAw', + docsUrl: + 'https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac', + slug: 'role-based-access-control', + }, + // Analytics + { + title: 'Reports & Metrics', + subtitle: "Monitor your project's health with usage insights.", + description: ` +Supabase Reports provide comprehensive insights into your project's performance, usage patterns, and overall health. This feature offers a detailed view of various metrics, helping you optimize your application, troubleshoot issues, and make data-driven decisions about resource allocation and scaling. + +## Key benefits +1. Performance monitoring: Track query performance, API response times, and overall system health. +2. Usage insights: Understand how your database, storage, and other resources are being utilized. +3. Cost optimization: Identify opportunities to optimize resource usage and reduce costs. +4. Trend analysis: Observe usage patterns over time to predict future needs and potential issues. +5. Security overview: Monitor authentication attempts, failed queries, and other security-related metrics. +6. Customizable dashboards: Create tailored views of the metrics most important to your team. + +## Reports are particularly valuable for: +- Development teams needing to optimize application performance +- DevOps professionals monitoring system health and resource utilization +- Project managers tracking usage growth and planning for scaling +- Financial teams managing cloud spending and optimizing costs +- Security personnel monitoring for unusual activity or potential threats +- Any stakeholder requiring insights into the project's operational status + +By leveraging Supabase Reports, you gain a understanding of your project's performance and usage patterns. This feature empowers you to make informed decisions about scaling, optimization, and resource allocation, ensuring that your application runs efficiently and cost-effectively. Whether you're troubleshooting issues, planning for growth, or optimizing costs, Supabase Reports provides the insights you need to manage your project effectively. +`, + icon: BarChart, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '/images/features/reports-and-metrics.png', + heroImageLight: '/images/features/reports-and-metrics-light.png', + docsUrl: 'https://supabase.com/blog/supabase-reports-and-metrics', + slug: 'reports-and-metrics', + }, + { + title: 'SOC 2 Compliance', + subtitle: 'Build with confidence on a SOC 2 compliant platform.', + description: ` +Supabase's SOC 2 Compliance demonstrates its commitment to maintaining the highest standards of security, availability, and confidentiality. This certification assures users that Supabase follows strict information security policies and procedures, encompassing the security, availability, processing, integrity, and confidentiality of customer data. + +## Key benefits +1. Trust and credibility: Build on a platform that has been independently audited for security practices. +2. Risk mitigation: Reduce the risk of data breaches and security incidents. +3. Compliance support: Easier compliance with your own regulatory requirements. +4. Standardized processes: Benefit from Supabase's adherence to industry-standard security practices. +5. Continuous improvement: Supabase's commitment to maintaining SOC 2 compliance ensures ongoing security enhancements. +6. Transparency: Access to SOC 2 reports provides insight into Supabase's security controls. +7. Competitive advantage: Use Supabase's compliance as a selling point for your own services. + +## SOC 2 Compliance is particularly valuable for: +- Enterprises with strict vendor security requirements +- Startups looking to build credibility with enterprise clients +- Financial technology companies handling sensitive financial data +- Healthcare applications dealing with protected health information +- Government contractors requiring compliant cloud services +- Any organization prioritizing data security and privacy in their applications + +Leveraging Supabase's SOC 2 Compliance involves: +1. Reviewing Supabase's SOC 2 report to understand its security practices +2. Aligning your application's security practices with Supabase's standards +3. Utilizing Supabase's compliant features to enhance your own security posture +4. Communicating the benefits of building on a SOC 2 compliant platform to stakeholders + +By building on Supabase's SOC 2 compliant platform, you gain a significant advantage in terms of security and trust. This compliance demonstrates to your users, partners, and regulators that you take data protection seriously and have chosen a backend platform that adheres to rigorous security standards. Whether you're a startup looking to win enterprise clients or an established company aiming to enhance your security posture, Supabase's SOC 2 compliance provides a solid foundation for building secure, trustworthy applications. +`, + icon: ShieldCheck, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '', + docsUrl: 'https://supabase.com/docs/guides/security/compliance', + slug: 'soc-2-compliance', + }, + // Studio + { + title: 'AI-Powered Query Assistance', + subtitle: 'Get help crafting complex SQL queries effortlessly.', + description: ` +The AI-Powered Query Assistance feature in Supabase provides users with intelligent suggestions for writing SQL queries based on natural language input. This tool is designed to enhance productivity and streamline the development process, making it easier for both novice and experienced developers to interact with their databases. By transforming user queries into optimized SQL commands, it reduces the time spent on query formulation and minimizes errors. + +## Key benefits +1. User-Friendly: Simplifies the query-writing process by allowing users to express their needs in plain language. +2. Time-Saving: Accelerates development by quickly generating accurate SQL queries. +3. Learning Tool: Acts as a learning resource for new developers to understand SQL syntax and best practices. +4. Error Reduction: Minimizes common mistakes in query formulation through intelligent suggestions. +5. Integration: Seamlessly integrates with existing Supabase projects, enhancing the overall user experience. + +This feature is especially beneficial for teams looking to improve their database interaction efficiency while ensuring that all members can contribute effectively, regardless of their SQL expertise. +`, + icon: Brain, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: 'https://www.youtube-nocookie.com/embed/hu2SQjvCXIw', + docsUrl: + 'https://supabase.com/blog/studio-introducing-assistant#introducing-the-supabase-assistant', + slug: 'ai-query-assistance', + }, + { + title: 'Logs & Analytics', + subtitle: 'Gain insights into your application’s performance and usage.', + description: ` +The Logs & Analytics feature in Supabase provides users with comprehensive logging and analytics capabilities, powered by Logflare. This tool enables developers to track and analyze log events from various Supabase services, such as the API gateway, Postgres databases, Storage, Edge Functions, and more. By leveraging a multi-node Elixir cluster, Supabase processes billions of log events daily, ensuring that users have access to critical insights for optimizing their applications. + +## Key benefits +1. Real-Time Monitoring: Access live data on application performance and user interactions to make informed decisions. +2. Comprehensive Log Management: Ingest and store logs from multiple sources, allowing for centralized management of application events. +3. Powerful Querying: Utilize SQL queries through Logflare Endpoints to analyze logs efficiently, enabling users to extract meaningful metrics. +4. Customizable Dashboards: Create tailored views within Supabase Studio to visualize log data and track key performance indicators (KPIs). +5. Scalability: Handle large volumes of log data with a robust infrastructure designed for high availability and performance. + +This feature is particularly valuable for teams looking to enhance their application's reliability and performance by gaining deeper insights into usage patterns and potential issues. +`, + icon: Activity, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '/images/features/logs-analytics.png', + heroImageLight: '/images/features/logs-analytics-light.png', + docsUrl: 'https://supabase.com/docs/guides/monitoring-troubleshooting/logs', + slug: 'logs-analytics', + }, + { + title: 'Visual Schema Designer', + subtitle: 'Design your database schema with an intuitive interface.', + description: ` +The Visual Schema Designer feature allows users to create and modify database schemas through a user-friendly drag-and-drop interface. This tool is aimed at simplifying the database design process, making it accessible for users who may not be familiar with traditional SQL commands. By visualizing relationships between tables and fields, users can efficiently plan their database architecture. + +## Key benefits +1. Intuitive Design: Facilitates easy schema creation through visual representation. +2. Relationship Mapping: Clearly illustrates how tables are interconnected, aiding in effective database design. +3. Accessibility: Makes database management approachable for non-technical users. +4. Real-Time Updates: Automatically reflects changes in the schema, ensuring consistency across the project. +5. Collaboration: Enhances teamwork by allowing multiple users to contribute to schema design simultaneously. + +This feature is particularly valuable for teams engaged in agile development processes where rapid iteration and collaboration are essential. +`, + icon: RectangleEllipsis, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: '/images/features/visual-schema-designer.png', + heroImageLight: '/images/features/visual-schema-designer-light.png', + docsUrl: 'https://supabase.com/blog/supabase-studio-3-0#schema-visualizer', + slug: 'visual-schema-designer', + }, + { + title: 'Policy Templates', + subtitle: 'Quickly implement common security policies.', + description: ` +Policy Templates provide a library of predefined security policies that can be easily applied to your Supabase projects. This feature streamlines the process of establishing role-based access control and other security measures, ensuring that best practices are followed consistently across applications. + +## Key benefits +1. Time Efficient: Reduces the time required to set up security policies by offering ready-made templates. +2. Best Practices: Ensures adherence to industry standards for data security and access control. +3. Customizable: Allows users to modify templates to fit specific project needs while maintaining a strong security foundation. +4. User-Friendly: Simplifies policy application through an intuitive interface within the Supabase Studio. +5. Documentation Support: Each template comes with comprehensive documentation to guide users through implementation. + +This feature is essential for organizations looking to maintain robust security protocols without extensive manual configuration efforts. +`, + icon: ShieldPlus, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: '/images/features/policy-templates.png', + heroImageLight: '/images/features/policy-templates-light.png', + docsUrl: '', + slug: 'policy-templates', + }, + { + title: 'SQL Editor', + subtitle: 'A powerful interface for writing and executing SQL queries.', + description: ` +The SQL Editor in Supabase Studio provides users with a robust platform for writing, executing, and managing SQL queries directly within their browser. This feature is designed to enhance productivity by offering syntax highlighting, auto-completion, and error detection, making it easier for developers to interact with their databases efficiently. + +## Key benefits +1. User-Friendly Interface: Intuitive design that simplifies the process of writing SQL queries. +2. Syntax Highlighting: Enhances readability by color-coding SQL syntax, helping users quickly identify errors. +3. Auto-Completion: Speeds up query writing by suggesting table names, column names, and functions. +4. Execution History: Keeps track of previously executed queries for easy reference and reuse. +5. Error Detection: Provides immediate feedback on syntax errors, reducing debugging time. + +This feature is particularly valuable for developers looking to streamline their workflow while ensuring accurate and efficient database interactions. +`, + icon: FileCode2, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: '/images/features/sql-editor.png', + heroImageLight: '/images/features/sql-editor-light.png', + docsUrl: '', + slug: 'sql-editor', + }, + { + title: 'Security & Performance Advisor', + subtitle: 'Optimize your database security and performance effortlessly.', + description: ` +The Security & Performance Advisor feature in Supabase offers users actionable insights into their database's security posture and performance metrics. By analyzing configurations and usage patterns, this tool identifies potential vulnerabilities and performance bottlenecks, providing recommendations for improvements. + +## Key benefits +1. Proactive Security Checks: Regular assessments of security settings to identify vulnerabilities. +2. Performance Optimization: Analyzes query performance to recommend optimizations that enhance efficiency. +3. User-Friendly Dashboard: Presents findings in an easily digestible format within Supabase Studio. +4. Actionable Recommendations: Provides clear steps for addressing identified issues, empowering users to enhance their database environments. +5. Ongoing Monitoring: Continuously evaluates changes in database usage to adapt recommendations accordingly. + +This feature is essential for organizations aiming to maintain high security standards while ensuring optimal performance across their applications. +`, + icon: ShieldPlus, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: '/images/features/security-and-performance-advisor.png', + heroImageLight: '/images/features/security-and-performance-advisor-light.png', + docsUrl: 'https://supabase.com/blog/security-and-performance-advisor', + slug: 'security-and-performance-advisor', + }, + { + title: 'Postgres Roles', + subtitle: 'Managing access to your Postgres database and configuring permissions.', + description: ` +Postgres Roles are a fundamental aspect of managing access permissions within your Supabase database. Roles can function as individual users or groups of users, allowing for flexible permission management. This feature is essential for setting up secure access to your database while enabling efficient collaboration among team members. + +Key benefits: +1. Granular Access Control: Configure permissions for various database objects, including tables, views, and functions, using the GRANT command. +2. Role Hierarchy: Organize roles in a hierarchy to simplify permission management, allowing child roles to inherit permissions from parent roles. +3. Secure User Management: Create roles with specific login privileges and strong passwords to ensure secure access to your database. +4. Revocation of Permissions: Easily revoke permissions using the REVOKE command, providing control over who has access to what within your database. +5. Predefined Roles: Supabase extends Postgres with a set of predefined roles, simplifying the initial setup for new projects. + +This feature is particularly valuable for teams looking to implement robust security measures while maintaining flexibility in how users interact with their database. +`, + icon: Users, + products: [PRODUCT_SHORTNAMES.DATABASE, ADDITIONAL_PRODUCTS.STUDIO], + heroImage: '/images/features/postgres-roles.png', + heroImageLight: '/images/features/postgres-roles-light.png', + docsUrl: 'https://supabase.com/docs/guides/database/postgres/roles', + slug: 'postgres-roles', + }, + { + title: 'Foreign Key Selector', + subtitle: 'Easily manage foreign key relationships between tables.', + description: ` +The Foreign Key Selector feature simplifies the process of establishing and managing foreign key relationships within your database schema. By providing a visual interface for selecting foreign keys, this tool enhances usability and reduces the likelihood of errors during schema design. + +## Key benefits +1. Visual Management: Allows users to easily visualize and select foreign key relationships between tables. +2. Error Reduction: Minimizes mistakes associated with manual foreign key configuration. +3. Streamlined Workflow: Enhances the efficiency of schema design by simplifying complex relationships. +4. Real-Time Updates: Automatically reflects changes made in the foreign key relationships throughout the project. +5. Documentation Support: Provides contextual information about foreign keys to guide users during setup. + +This feature is particularly beneficial for developers working with complex data models who need a straightforward way to manage relational integrity within their databases. +`, + icon: DatabaseZap, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: + 'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/fk-lookup.mp4', + docsUrl: 'https://supabase.com/blog/supabase-studio-2.0#foreign-key-selector', + slug: 'foreign-key-selector', + }, + { + title: 'Log Drains', + subtitle: 'Export logs to external destinations for enhanced monitoring.', + description: ` +Log Drains enable developers to export logs generated by Supabase products—such as the Database, Storage, Realtime, and Auth—to external destinations like Datadog or custom HTTP endpoints. This feature provides a unified view of logs within existing logging and monitoring systems, allowing teams to build robust alerting and observability pipelines. + +## Key benefits +1. Centralized Logging: Consolidate logs from multiple Supabase services into a single location for easier management and analysis. +2. Custom Alerting: Ingest logs into Security Information and Event Management (SIEM) or Intrusion Detection Systems (IDS) to create tailored alerting rules based on database events. +3. Extended Retention: Supports longer log retention periods to meet compliance requirements, ensuring data availability for audits and investigations. +4. Flexible Configuration: Easily set up Log Drains through the project settings, with support for popular destinations like Datadog and custom HTTP endpoints. +5. Scalable Architecture: Built on Logflare's multi-node Elixir cluster, allowing for efficient and scalable log dispatching to multiple destinations. + +This feature is particularly useful for teams seeking to enhance their observability practices while maintaining compliance and security standards across their applications. +`, + icon: Activity, + products: [ADDITIONAL_PRODUCTS.STUDIO], + heroImage: 'https://www.youtube-nocookie.com/embed/A4GFmvgxS-E', + docsUrl: 'https://supabase.com/blog/log-drains', + slug: 'log-drains', + }, +] diff --git a/apps/www/internals/generate-sitemap.mjs b/apps/www/internals/generate-sitemap.mjs index 6fd2fc4333a..abe470b5b99 100644 --- a/apps/www/internals/generate-sitemap.mjs +++ b/apps/www/internals/generate-sitemap.mjs @@ -28,6 +28,7 @@ async function generate() { '!pages/404.tsx', '.next/server/pages/partners/integrations/*.html', '.next/server/pages/partners/experts/*.html', + '.next/server/pages/features/*.html', ]) const blogUrl = 'blog' @@ -62,6 +63,7 @@ async function generate() { if (route === '/case-studies/[slug]') return null if (route === '/customers/[slug]') return null if (route === '/events/[slug]') return null + if (route === '/features/[slug]') return null if (route === '/blog/categories/[category]') return null if (route === '/partners/experts/[slug]') return null if (route === '/partners/integrations/[slug]') return null diff --git a/apps/www/pages/features.tsx b/apps/www/pages/features.tsx new file mode 100644 index 00000000000..a8654f8f666 --- /dev/null +++ b/apps/www/pages/features.tsx @@ -0,0 +1,219 @@ +import React, { useEffect, useState, useCallback } from 'react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import { motion } from 'framer-motion' +import { Search } from 'lucide-react' +import debounce from 'lodash/debounce' + +import { Button, Checkbox, cn, Input } from 'ui' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import Panel from '~/components/Panel' + +import { features } from '~/data/features' + +function FeaturesPage() { + const router = useRouter() + const { basePath, query } = router + const [searchTerm, setSearchTerm] = useState((query.q as string) || '') + const [selectedProducts, setSelectedProducts] = useState( + (query.products as string)?.split(',') || [] + ) + + const HAS_ACTIVE_FILTERS = selectedProducts.length || searchTerm.length + + const products = Array.from(new Set(features.flatMap((feature) => feature.products))) + + // Debounced function to update URL params + const updateQueryParamsDebounced = useCallback( + debounce(() => updateQueryParams(), 300), + [searchTerm, selectedProducts] + ) + + const updateQueryParams = () => { + const params = new URLSearchParams() + if (searchTerm) params.set('q', searchTerm) + if (selectedProducts.length > 0) params.set('products', selectedProducts.join(',')) + + router.replace({ pathname: '/features', query: params.toString() }, undefined, { + shallow: true, + }) + } + + // Apply filters based on initial URL params on mount + useEffect(() => { + updateQueryParamsDebounced() + return updateQueryParamsDebounced.cancel // Cleanup on unmount + }, [searchTerm, selectedProducts, updateQueryParamsDebounced]) + + // Sync state with query parameters when they change + useEffect(() => { + if (query.q !== searchTerm) setSearchTerm((query.q as string) || '') + if (query.products !== selectedProducts.join(',')) { + setSelectedProducts((query.products as string)?.split(',') || []) + } + }, [query.q, query.products]) + + // Handle search input change + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value) + } + + // Handle product checkbox change + const handleProductChange = (product: string) => { + setSelectedProducts((prev) => + prev.includes(product) ? prev.filter((p) => p !== product) : [...prev, product] + ) + } + + // Filter features based on search term and selected products + const filteredFeatures = features.filter((feature) => { + const matchesSearch = + feature.title.toLowerCase().includes(searchTerm.toLowerCase()) || + feature.subtitle.toLowerCase().includes(searchTerm.toLowerCase()) || + feature.description.toLowerCase().includes(searchTerm.toLowerCase()) + + const matchesProduct = + selectedProducts.length === 0 || + feature.products.some((product) => selectedProducts.includes(product)) + + return matchesSearch && matchesProduct + }) + + const meta = { + title: 'Supabase Features', + description: + 'From authentication to storage, everything you need to build and ship your next project.', + } + + return ( + <> + + + +
      + +

      Supabase Features

      +

      + Everything you need
      to build and ship your next + project. +

      +
      +
      +
      + +
      +
      + } + size="small" + autoComplete="off" + type="search" + placeholder="Search features" + value={searchTerm} + onChange={handleSearchChange} + className="w-full [&_input]:text-base [&_input]:md:text-sm [&_input]:!leading-4" + /> +
      +

      Filter by tags:

      +
      + {products + .sort((a, b) => (a > b ? 1 : -1)) + .map((product) => ( +
      + handleProductChange(product)} + className="[&_input]:m-0" + /> + +
      + ))} +
      +
      + Features selected: {filteredFeatures.length} +
      +
      + +
      +
      +
      + {!filteredFeatures?.length ? ( +

      + No features found with these filters +

      + ) : ( +
      + {filteredFeatures + .sort((a, b) => (a.title > b.title ? 1 : -1)) + .map((feature) => ( + + +
      + +
      +
      +

      + {feature.title} +

      +

      + {feature.subtitle} +

      +
      +
      + + ))} +
      + )} +
      +
      +
      + + ) +} + +export default FeaturesPage diff --git a/apps/www/pages/features/[slug].tsx b/apps/www/pages/features/[slug].tsx new file mode 100644 index 00000000000..db79dc3f6f4 --- /dev/null +++ b/apps/www/pages/features/[slug].tsx @@ -0,0 +1,237 @@ +import { ChevronLeft, ChevronRight, LucideIcon } from 'lucide-react' +import { GetStaticPaths, GetStaticProps } from 'next' +import { NextSeo } from 'next-seo' +import Link from 'next/link' +import ReactMarkdown from 'react-markdown' + +import { Badge, Button, cn, Image } from 'ui' +import DefaultLayout from '~/components/Layouts/Default' +import ShareArticleActions from '~/components/Blog/ShareArticleActions' +import SectionContainer from '~/components/Layouts/SectionContainer' +import CTABanner from '~/components/CTABanner' +import PrevNextFeatureNav from '~/components/PrevNextFeatureNav' + +import { features } from '~/data/features' +import type { FeatureType } from '~/data/features' + +interface FeaturePageProps { + feature: FeatureType + prevFeature: FeatureType | null + nextFeature: FeatureType | null +} + +export const getStaticPaths: GetStaticPaths = async () => { + // Generate paths based on each feature's slug + const paths = features.map((feature) => ({ + params: { slug: feature.slug }, + })) + return { paths, fallback: false } +} + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const { slug } = params as { slug: string } + const featureIndex = features.findIndex((feature) => feature.slug === slug) + + if (featureIndex === -1) { + return { notFound: true } + } + + const feature = features[featureIndex] + const prevFeature = featureIndex > 0 ? features[featureIndex - 1] : features[features.length - 1] + const nextFeature = featureIndex < features.length - 1 ? features[featureIndex + 1] : features[0] + + // Destructure the icon property out, and add the icon name to props instead + const { icon, ...featureWithoutIcon } = feature + const { icon: _prevIcon, ...prevFeatureWithoutIcon } = prevFeature + const { icon: _nextIcon, ...nextFeatureWithoutIcon } = nextFeature + + return { + props: { + feature: featureWithoutIcon, + prevFeature: prevFeatureWithoutIcon, + nextFeature: nextFeatureWithoutIcon, + }, + } +} + +const FeaturePage: React.FC = ({ feature, prevFeature, nextFeature }) => { + const meta = { + title: `${feature.title} | Supabase Features`, + description: feature.subtitle, + url: `https://supabase.com/features/${feature.slug}`, + // image: ogImageUrl, + } + + const Icon = features.find((f) => f.slug === feature.slug)?.icon as LucideIcon + + return ( + <> + + +
      + +
      + +
      +
      + {feature.products.map((product) => ( + + + {product} + + + ))} +
      +

      {feature.title}

      +

      {feature.subtitle}

      +
      +
      + {!!feature.heroImage ? ( + feature.heroImage.includes('.mp4') ? ( + + ) : feature.heroImage.includes('.youtube') ? ( +
      +