mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
Merge branch 'master' into dnywh/prototype/warehouse
# Conflicts: # apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx # apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.utils.test.ts # apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.utils.ts # apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx
This commit is contained in:
commit
60a0ddffc9
374 files changed
+8222
-2152
No files matched your search
+3
-1
@@ -8,6 +8,8 @@
|
||||
|
||||
/apps/studio/ @supabase/Dashboard
|
||||
|
||||
/apps/docs/ @supabase/docs
|
||||
|
||||
/apps/www/ @supabase/marketing
|
||||
/apps/www/public/images/blog @supabase/marketing
|
||||
/apps/www/lib/redirects.js
|
||||
@@ -19,4 +21,4 @@
|
||||
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
|
||||
/apps/studio/pages/new/index.tsx @supabase/security
|
||||
|
||||
/packages/shared-data/compute-disk-limits.ts @supabase/infra @supabase/platform
|
||||
/packages/shared-data/compute-disk-limits.ts @supabase/platform
|
||||
@@ -15,8 +15,42 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Cheap, always-on gate: verifies the eval harness can reach the MCP server
|
||||
# (its one real tool, search_docs). Runs on every push/PR — no OpenAI, no full
|
||||
# eval suite — so a broken MCP connection is caught early with an actionable
|
||||
# message. The eval job depends on this, so evals never run against a broken
|
||||
# MCP connection.
|
||||
preflight:
|
||||
name: Eval MCP preflight
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Preflight — MCP connectivity
|
||||
run: cd apps/studio && pnpm evals:preflight
|
||||
|
||||
eval:
|
||||
name: Run evals
|
||||
needs: preflight
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals') && github.event.pull_request.head.repo.full_name == github.repository)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
@@ -43,8 +77,8 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"tailwindcss": "catalog:",
|
||||
"tsconfig": "workspace:*",
|
||||
"tsx": "catalog:",
|
||||
"@typescript/native": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unist-builder": "3.0.0"
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
--code-token-keyword: #6b35dc;
|
||||
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-constant: #15593b;
|
||||
--code-token-string: #f1a10d;
|
||||
--code-token-string: #c46a0a;
|
||||
--code-token-comment: #7e7e7e;
|
||||
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-function: #15593b;
|
||||
--code-token-string-expression: #f1a10d;
|
||||
--code-token-string-expression: #c46a0a;
|
||||
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
|
||||
|
||||
@@ -7,7 +7,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
|
||||
>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground font-medium">What you can do with the Metrics API</span>}
|
||||
header="What you can do with the Metrics API"
|
||||
id="how-do-i-check-when-a-user-went-through-mfa"
|
||||
className="border-0 px-2 py-4"
|
||||
>
|
||||
|
||||
@@ -25,72 +25,33 @@ using (true);`
|
||||
|
||||
## 1. Create a Supabase project
|
||||
|
||||
Before you can use Supabase, you need a Supabase project. You can create a project visually in the Dashboard or programmatically using [the Management API](/docs/reference/api/introduction).
|
||||
|
||||
<Tabs scrollable size="small" type="underlined" defaultActiveId="dashboard" queryGroup="create-project">
|
||||
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
To start, you need a Supabase project.
|
||||
|
||||
Create a new Supabase project from [the Dashboard of any organization](/dashboard/new/_) you belong to.
|
||||
|
||||
<Admonition type="tip">
|
||||
<Admonition type="tip" title="Want to create a project programmatically?">
|
||||
|
||||
You can also use [database.new](https://database.new) to create a new Supabase project.
|
||||
Use [the Management API](/docs/reference/api/v1-create-a-project) or ask [the MCP server](/docs/guides/ai-tools/mcp#account-management) to create a new Supabase project.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="management-api" label="Management API">
|
||||
|
||||
First, get your access token from the [**Account Settings > Access Tokens**](/dashboard/account/tokens) section of the Dashboard.
|
||||
|
||||
Then create a project using [the Management API](/docs/reference/api/introduction):
|
||||
|
||||
```bash
|
||||
export SUPABASE_ACCESS_TOKEN="your-access-token"
|
||||
|
||||
# List your organizations to get the organization ID
|
||||
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
||||
https://api.supabase.com/v1/organizations
|
||||
|
||||
# Create a new project (replace <org-id> with your organization ID)
|
||||
curl -X POST https://api.supabase.com/v1/projects \
|
||||
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"organization_id": "<org-id>",
|
||||
"name": "My Project",
|
||||
"region": "us-east-1",
|
||||
"db_pass": "<your-secure-password>"
|
||||
}'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## 2. Install agent skills (optional)
|
||||
|
||||
[Agent Skills](/docs/guides/ai-tools/ai-skills) are curated instruction sets that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
Install them in your project with:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 3. Set up your database
|
||||
## 2. Set up your database
|
||||
|
||||
When your Supabase project is up and running, create an `instruments` table with some sample data. Then set only the privileges each Postgres role needs, add [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default, and create an RLS policy to make the data in the table publicly readable.
|
||||
|
||||
Do these steps within your project's dashboard by copying and running the snippet in your project's [SQL Editor](/dashboard/project/_/sql/new).
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Save some steps by <a href={`/dashboard/project/_/sql/new?content=${encodeURIComponent(sqlSetup)}`}>clicking here to prefill the SQL</a> in the SQL Editor, and then clicking **Run**.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Or do this manually in your project's [SQL Editor](/dashboard/project/_/sql/new), by pasting the SQL below, and clicking **Run**.
|
||||
<Admonition type="tip" title="Want to setup the database programmatically?">
|
||||
|
||||
You can use [the Management API](/docs/reference/api/v1-run-a-query) or ask [the MCP server](/docs/guides/ai-tools/mcp#database) to execute SQL queries.
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sql SQL_EDITOR
|
||||
-- Create the table
|
||||
|
||||
@@ -41,7 +41,7 @@ Embeddings compress discrete information (words & symbols) into distributed cont
|
||||
|
||||
The chart below plots example phrases as points. Phrases with similar meanings sit close together, and unrelated phrases sit far apart.
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/ai/vector-similarity.png"
|
||||
alt="A two-dimensional chart plotting example phrases as points, where phrases with similar meanings sit close together and unrelated phrases sit far apart."
|
||||
width="640"
|
||||
|
||||
@@ -6,7 +6,7 @@ subtitle: 'Going to production checklist for AI applications.'
|
||||
sidebar_label: 'Going to Production'
|
||||
---
|
||||
|
||||
This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
|
||||
This guide helps you prepare your application for production. It provides actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
|
||||
|
||||
See our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide for more information about engineering at scale.
|
||||
|
||||
@@ -72,7 +72,7 @@ The values of lists and probes directly affect accuracy and queries per second (
|
||||
- Higher `probes` means that select queries will be slower, but you can achieve better accuracy.
|
||||
- `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same accuracy.
|
||||
|
||||
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blogpost.
|
||||
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blog post.
|
||||
|
||||
The chart below shows how the IVFFlat lists count affects accuracy and queries-per-second.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Start by visiting [colab.research.google.com](https://colab.research.google.com/
|
||||
|
||||
We'll use the Supabase Vector client, [Vecs](/docs/guides/ai/vecs-python-client), to manage our collections.
|
||||
|
||||
At the top of the notebook add the notebook paste the following code and hit the "execute" button (`ctrl+enter`):
|
||||
At the top of the notebook, paste the following code and click "Execute" (`ctrl+enter`):
|
||||
|
||||
```py
|
||||
pip install vecs
|
||||
|
||||
@@ -149,7 +149,7 @@ curl --output result.jpg --location --request POST 'http://localhost:54321/funct
|
||||
|
||||
In this example, your generated image will save to `result.jpg`:
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
|
||||
alt="Llama wearing sunglasses example"
|
||||
width="400"
|
||||
|
||||
@@ -9,7 +9,7 @@ sidebar_label: 'LangChain'
|
||||
|
||||
## Initializing your database
|
||||
|
||||
Prepare you database with the relevant tables:
|
||||
Prepare your database with the relevant tables:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -220,7 +220,7 @@ export const run = async () => {
|
||||
|
||||
LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
|
||||
|
||||
You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search).
|
||||
You can install the LangChain Hybrid Search function through our [database.dev package manager](https://database.dev/langchain/hybrid_search).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
id: 'ai-rag-with-permissions'
|
||||
title: 'RAG with Permissions'
|
||||
subtitle: 'Fine-grain access control with Retrieval Augmented Generation.'
|
||||
description: 'Implement fine-grain access control with retrieval augmented generation'
|
||||
subtitle: 'Fine-grained access control with Retrieval Augmented Generation.'
|
||||
description: 'Implement fine-grained access control with retrieval augmented generation'
|
||||
sidebar_label: 'RAG with Permissions'
|
||||
---
|
||||
|
||||
Since pgvector is built on top of Postgres, you can implement fine-grain access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
|
||||
Since pgvector is built on top of Postgres, you can implement fine-grained access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
|
||||
|
||||
Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG).
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to
|
||||
|
||||
## Embedding models
|
||||
|
||||
There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
There are many embedding models available today. Supabase Edge Functions has [built-in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
|
||||
It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.
|
||||
|
||||
@@ -65,7 +65,7 @@ For more details on vector columns, including how to generate embeddings and sto
|
||||
|
||||
### Similarity metric
|
||||
|
||||
`pgvector` support 3 operators for computing distance between embeddings:
|
||||
`pgvector` supports 3 operators for computing distance between embeddings:
|
||||
|
||||
| **Operator** | **Description** |
|
||||
| ------------ | ---------------------- |
|
||||
@@ -143,7 +143,7 @@ You can also call this method directly from SQL:
|
||||
select *
|
||||
from match_documents(
|
||||
'[...]'::extensions.vector(512), -- pass the query embedding
|
||||
0.78, -- chose an appropriate threshold for your data
|
||||
0.78, -- choose an appropriate threshold for your data
|
||||
10 -- choose the number of matches
|
||||
);
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Learn how to use vectors within your own Postgres tables'
|
||||
sidebar_label: 'Vector columns'
|
||||
---
|
||||
|
||||
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
|
||||
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user, see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
|
||||
|
||||
Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a Postgres extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
@@ -48,7 +48,7 @@ To disable an extension, call `drop extension`.
|
||||
|
||||
### Create a table to store vectors
|
||||
|
||||
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parenthesis) represents the number of dimensions stored in that vector.
|
||||
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parentheses) represents the number of dimensions stored in that vector.
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
@@ -100,7 +100,7 @@ This example uses the JavaScript Supabase client, but you can modify it to work
|
||||
|
||||
### Querying a vector / embedding
|
||||
|
||||
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for computing distance:
|
||||
Similarity search is the most common use case for vectors. `pgvector` supports 3 new operators for computing distance:
|
||||
|
||||
| Operator | Description |
|
||||
| -------- | ---------------------- |
|
||||
|
||||
@@ -307,7 +307,7 @@ If your application uses the Supabase Database, Storage or Edge Functions, Row L
|
||||
>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">How do I check when a user went through MFA?</span>}
|
||||
header="How do I check when a user went through MFA?"
|
||||
id="how-do-i-check-when-a-user-went-through-mfa"
|
||||
>
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ The Proxy is responsible for:
|
||||
<Accordion>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">What does the `cookies` object do?</span>}
|
||||
header="What does the `cookies` object do?"
|
||||
id="utility-cookies"
|
||||
>
|
||||
|
||||
@@ -213,7 +213,7 @@ The Proxy is responsible for:
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">Do I need to create a new client for every route?</span>}
|
||||
header="Do I need to create a new client for every route?"
|
||||
id="client-deduplication"
|
||||
>
|
||||
|
||||
|
||||
@@ -452,6 +452,12 @@ create table private.salaries (
|
||||
);
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
If you want to access a custom schema through the Supabase Data API, you need to expose it and grant the appropriate permissions. See [Using Custom Schemas](/docs/guides/api/using-custom-schemas) for detailed steps. For security best practices around schema exposure, see [Securing your API](/docs/guides/api/securing-your-api).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Views
|
||||
|
||||
A View is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create an Astro app
|
||||
## 3. Create an Astro app
|
||||
|
||||
Create an Astro app using the `npm create` command.
|
||||
|
||||
@@ -15,6 +15,16 @@ npm create astro@latest my-app
|
||||
cd my-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install Supabase client library and Node adapter
|
||||
|
||||
Install the `supabase-js` client library and the `@astrojs/node` adapter to enable server-side rendering.
|
||||
@@ -39,11 +49,9 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Declare Supabase environment variables
|
||||
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=astro&tab=frameworks):
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=astro&tab=frameworks):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=astro">
|
||||
@@ -56,6 +64,8 @@ PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} />
|
||||
|
||||
## 8. Create a Supabase client helper
|
||||
|
||||
Create a utility file to initialize the Supabase client:
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create an Expo app
|
||||
## 3. Create an Expo app
|
||||
|
||||
Create a minimal Expo app using the `create-expo-app` command with the blank TypeScript template.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a minimal Expo app using the `create-expo-app` command with the blank Typ
|
||||
npx create-expo-app my-app --template blank-typescript
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `@supabase/supabase-js` client library which provides a convenient interface for working with Supabase from a React Native app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the Expo app and install `supabase-js` along with the required depen
|
||||
cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env` file in the root of your project and populate it with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true).
|
||||
Create a `.env` file in the root of your project and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true).
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
|
||||
@@ -41,6 +49,8 @@ EXPO_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 7. Initialize the Supabase client
|
||||
|
||||
Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Python app with Flask
|
||||
## 3. Create a Python app with Flask
|
||||
|
||||
Create a new directory for your Python app and set up a virtual environment.
|
||||
|
||||
@@ -16,6 +16,16 @@ python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install Flask and the Supabase client library
|
||||
|
||||
The fastest way to get started is to use Flask for the web framework and the `supabase-py` client library which provides a convenient interface for working with Supabase from a Python app.
|
||||
@@ -26,11 +36,9 @@ Install both packages using pip.
|
||||
pip install flask supabase
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 6. Create environment variables file
|
||||
|
||||
Create a `.env` file in your project root and populate it with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
|
||||
Create a `.env` file in your project root and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
|
||||
@@ -41,6 +49,8 @@ SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 7. Query data from the app
|
||||
|
||||
Install the `python-dotenv` package to load environment variables:
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Flutter app
|
||||
## 3. Create a Flutter app
|
||||
|
||||
Create a Flutter app using the `flutter create` command.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a Flutter app using the `flutter create` command.
|
||||
flutter create my_app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) client library which provides a convenient interface for working with Supabase from a Flutter app.
|
||||
@@ -24,11 +34,9 @@ Open the `pubspec.yaml` file inside your Flutter app and add `supabase_flutter`
|
||||
supabase_flutter: ^2.0.0
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} />
|
||||
|
||||
## 6. Initialize the Supabase client
|
||||
|
||||
Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=flutter&tab=mobiles):
|
||||
Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=flutter&tab=mobiles):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=flutter">
|
||||
@@ -50,6 +58,8 @@ Future<void> main() async {
|
||||
}
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} />
|
||||
|
||||
## 7. Query data from the app
|
||||
|
||||
Use a `FutureBuilder` to fetch the data when the home page loads and display the query result in a `ListView`.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Hono app
|
||||
## 3. Create a Hono app
|
||||
|
||||
Bootstrap the Hono example app from the Supabase Samples using the CLI.
|
||||
|
||||
@@ -14,6 +14,16 @@ Bootstrap the Hono example app from the Supabase Samples using the CLI.
|
||||
npx supabase@latest bootstrap hono
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The `package.json` file in the project includes the necessary dependencies, including `@supabase/supabase-js` and `@supabase/ssr` to help with server-side auth.
|
||||
@@ -22,12 +32,9 @@ The `package.json` file in the project includes the necessary dependencies, incl
|
||||
npm install
|
||||
```
|
||||
|
||||
{/* TODO: Not ideal for frameworks that have no entry in Connect */}
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Set up the required environment variables
|
||||
|
||||
Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&tab=frameworks).
|
||||
Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&tab=frameworks).
|
||||
|
||||
Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings.
|
||||
|
||||
@@ -35,6 +42,9 @@ Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
{/* TODO: Not ideal for frameworks that have no entry in Connect */}
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Start the app
|
||||
|
||||
Start the app, go to http://localhost:5173.
|
||||
|
||||
@@ -6,10 +6,20 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create an iOS SwiftUI app with Xcode
|
||||
## 3. Create an iOS SwiftUI app with Xcode
|
||||
|
||||
Select the **Xcode > New Project > iOS > App** menu item.
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
Add the [supabase-swift](https://github.com/supabase/supabase-swift) package to your app using the Swift Package Manager.
|
||||
@@ -18,11 +28,9 @@ In Xcode, navigate to **File > Add Package Dependencies...** and enter the repos
|
||||
|
||||
Make sure to add `Supabase` product package as a dependency to your application target.
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} />
|
||||
|
||||
## 6. Initialize the Supabase client
|
||||
|
||||
Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=swift&tab=mobiles):
|
||||
Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=swift&tab=mobiles):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=swift">
|
||||
@@ -39,6 +47,8 @@ let supabase = SupabaseClient(
|
||||
)
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} />
|
||||
|
||||
## 7. Create a data model for instruments
|
||||
|
||||
Create a decodable struct to deserialize the data from the database.
|
||||
|
||||
@@ -6,10 +6,20 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create an Android app with Android Studio
|
||||
## 3. Create an Android app with Android Studio
|
||||
|
||||
Select the **Android Studio > New > New Android Project** menu item.
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install dependencies
|
||||
|
||||
Open `build.gradle.kts` (app) file and add the serialization plugin, Ktor client, and Supabase client.
|
||||
@@ -46,15 +56,13 @@ Add the following line to the `AndroidManifest.xml` file under the `manifest` ta
|
||||
...
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} />
|
||||
|
||||
## 7. Initialize the Supabase client
|
||||
|
||||
You can create a Supabase client whenever you need to perform an API call.
|
||||
|
||||
For a quick example, create a client at the top of the `MainActivity.kt` file below the imports.
|
||||
|
||||
Replace the `supabaseUrl` and `supabaseKey` with your own, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=androidkotlin&tab=mobiles):
|
||||
Replace the `supabaseUrl` and `supabaseKey` with your own, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=androidkotlin&tab=mobiles):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=androidkotlin">
|
||||
@@ -74,6 +82,8 @@ val supabase = createSupabaseClient(
|
||||
...
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} />
|
||||
|
||||
## 8. Create a data model for instruments
|
||||
|
||||
Create a serializable data class to represent the data from the database.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Laravel project
|
||||
## 3. Create a Laravel project
|
||||
|
||||
Make sure your PHP and Composer versions are up to date, then use `composer create-project` to scaffold a new Laravel project.
|
||||
|
||||
@@ -16,6 +16,16 @@ See the [Laravel docs](https://laravel.com/docs/10.x/installation#creating-a-lar
|
||||
composer create-project laravel/laravel example-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the authentication template
|
||||
|
||||
Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a basic implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication).
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Next.js app
|
||||
## 3. Create a Next.js app
|
||||
|
||||
Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server), [TypeScript](https://www.typescriptlang.org/), and [Tailwind CSS](https://tailwindcss.com/).
|
||||
|
||||
@@ -14,11 +14,19 @@ Use the `create-next-app` command and the `with-supabase` template, to create a
|
||||
npx create-next-app -e with-supabase
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} />
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Declare Supabase environment variables
|
||||
|
||||
Rename `.env.example` to `.env.local` and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nextjs&tab=frameworks).
|
||||
Rename `.env.example` to `.env.local` and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nextjs&tab=frameworks).
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs">
|
||||
@@ -31,6 +39,8 @@ NEXT_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Query Supabase data from Next.js
|
||||
|
||||
Create a new file at `app/instruments/page.tsx` and populate with the following.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Nuxt app
|
||||
## 3. Create a Nuxt app
|
||||
|
||||
Create a Nuxt app using the `npx nuxi` command.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a Nuxt app using the `npx nuxi` command.
|
||||
npx nuxi@latest init my-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Nuxt app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the Nuxt app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nuxt&tab=frameworks):
|
||||
Create a `.env` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nuxt&tab=frameworks):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nuxt">
|
||||
@@ -56,6 +64,8 @@ export default defineNuxtConfig({
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Query data from the app
|
||||
|
||||
In `app.vue`, create a Supabase client using your config values and replace the existing content with the following code.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a React app
|
||||
## 3. Create a React app
|
||||
|
||||
Create a React app using a [Vite](https://vitejs.dev/guide/) template.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a React app using a [Vite](https://vitejs.dev/guide/) template.
|
||||
npm create vite@latest my-app -- --template react
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library, which provides a convenient interface for working with Supabase from a React app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the React app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env.local` file and populate it with your Supabase URL and publishable key that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=react&tab=frameworks)
|
||||
Create a `.env.local` file and populate it with your Supabase URL and publishable key that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=react&tab=frameworks)
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react">
|
||||
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Query data from the app
|
||||
|
||||
Replace the contents of `App.jsx` with a `getInstruments` function that fetches the data and displays the query result on the page using a Supabase client.
|
||||
|
||||
@@ -24,7 +24,7 @@ To do this, set the connection mode to `Transaction` in the [Database Settings p
|
||||
|
||||
To get the Session mode connection pooler string, change the port of the connection string from the dashboard to 5432.
|
||||
|
||||
You will need the Transaction mode connection string and the Session mode connection string to set up environment variables in Step 4.
|
||||
You will need the Transaction mode connection string and the Session mode connection string to set up environment variables in Step 6.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
@@ -50,7 +50,21 @@ While TypeScript is recommended, If you want a JavaScript app, omit the `--ts` f
|
||||
yarn create redwood-app my-app --ts
|
||||
```
|
||||
|
||||
## 4. Configure environment variables
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install MCP server (optional)
|
||||
|
||||
The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp).
|
||||
|
||||
## 6. Configure environment variables
|
||||
|
||||
In your `.env` file, add the following environment variables for your database connection:
|
||||
|
||||
@@ -66,11 +80,11 @@ DATABASE_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabas
|
||||
DIRECT_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:5432/postgres"
|
||||
```
|
||||
|
||||
## 5. Update your Prisma schema
|
||||
## 7. Update your Prisma schema
|
||||
|
||||
By default, RedwoodJS ships with a SQLite database, but we want to use Postgres.
|
||||
|
||||
Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you set up in Step 4.
|
||||
Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you set up in Step 6.
|
||||
|
||||
```prisma name=api/db/schema.prisma
|
||||
datasource db {
|
||||
@@ -80,7 +94,7 @@ datasource db {
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Create the instrument model and apply a schema migration
|
||||
## 8. Create the instrument model and apply a schema migration
|
||||
|
||||
Create the Instrument model in `api/db/schema.prisma` and then run `yarn rw prisma migrate dev` from your terminal to apply the migration.
|
||||
|
||||
@@ -91,7 +105,7 @@ model Instrument {
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Update seed script
|
||||
## 9. Update seed script
|
||||
|
||||
Seed the database with a few instruments.
|
||||
|
||||
@@ -120,7 +134,7 @@ export default async () => {
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Seed your database
|
||||
## 10. Seed your database
|
||||
|
||||
Run the seed database command to populate the `Instrument` table with the instruments you created.
|
||||
|
||||
@@ -134,7 +148,7 @@ The reset database command `yarn rw prisma db reset` recreates the tables and al
|
||||
yarn rw prisma db seed
|
||||
```
|
||||
|
||||
## 9. Scaffold the instrument UI
|
||||
## 11. Scaffold the instrument UI
|
||||
|
||||
Use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model.
|
||||
|
||||
@@ -142,13 +156,13 @@ Use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model.
|
||||
yarn rw g scaffold instrument
|
||||
```
|
||||
|
||||
## 10. Start the app
|
||||
## 12. Start the app
|
||||
|
||||
Start the app via `yarn rw dev`. A browser will open to the RedwoodJS Splash page.
|
||||
|
||||

|
||||
|
||||
## 11. View instruments UI
|
||||
## 13. View instruments UI
|
||||
|
||||
Click on `/instruments` to visit http://localhost:8910/instruments where should see the list of instruments.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Refine app
|
||||
## 3. Create a Refine app
|
||||
|
||||
Create a [Refine](https://github.com/refinedev/refine) app using the [create refine-app](https://refine.dev/docs/getting-started/quickstart/).
|
||||
|
||||
@@ -18,11 +18,19 @@ npm create refine-app@latest -- --preset refine-supabase my-app
|
||||
|
||||

|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} />
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Update `supabaseClient` with environment variables
|
||||
|
||||
Update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=refine&tab=frameworks). The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend.
|
||||
Update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=refine&tab=frameworks). The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend.
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=refine">
|
||||
@@ -46,6 +54,8 @@ export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
|
||||
})
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Add instruments resource and pages
|
||||
|
||||
Use the following code to automatically add resources and generate code for the pages to show the `instruments` data using Refine Inferencer.
|
||||
|
||||
@@ -16,7 +16,19 @@ Check the [Rails docs](https://guides.rubyonrails.org/getting_started.html) for
|
||||
rails new blog -d=postgresql
|
||||
```
|
||||
|
||||
## 2. Set up the Postgres connection details
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 3. Install MCP server (optional)
|
||||
|
||||
The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp).
|
||||
|
||||
## 4. Set up the Postgres connection details
|
||||
|
||||
Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely.
|
||||
|
||||
@@ -34,7 +46,7 @@ If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/
|
||||
export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
|
||||
```
|
||||
|
||||
## 3. Create and run a database migration
|
||||
## 5. Create and run a database migration
|
||||
|
||||
Rails includes Active Record as the ORM as well as database migration tooling which generates the SQL migration files for you.
|
||||
|
||||
@@ -45,7 +57,7 @@ bin/rails generate model Article title:string body:text
|
||||
bin/rails db:migrate
|
||||
```
|
||||
|
||||
## 4. Use the model to interact with the database
|
||||
## 6. Use the model to interact with the database
|
||||
|
||||
You can use the included Rails console to interact with the database. For example, you can create new entries or list all entries in a Model's table.
|
||||
|
||||
@@ -60,7 +72,7 @@ article.save # Saves the entry to the database
|
||||
Article.all
|
||||
```
|
||||
|
||||
## 5. Start the app
|
||||
## 7. Start the app
|
||||
|
||||
Run the development server. Go to http://127.0.0.1:3000 in a browser to see your application running.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a SolidJS app
|
||||
## 3. Create a SolidJS app
|
||||
|
||||
Create a SolidJS app using the `degit` command.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a SolidJS app using the `degit` command.
|
||||
npx degit solidjs/templates/js my-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SolidJS app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the SolidJS app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=solidjs&tab=frameworks):
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=solidjs&tab=frameworks):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=solidjs">
|
||||
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Query data from the app
|
||||
|
||||
In `App.jsx`, create a Supabase client to fetch the instruments data.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a SvelteKit app
|
||||
## 3. Create a SvelteKit app
|
||||
|
||||
Create a SvelteKit app using the `npm create` command.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a SvelteKit app using the `npm create` command.
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SvelteKit app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the SvelteKit app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env` file at the root of your project and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=sveltekit&tab=frameworks):
|
||||
Create a `.env` file at the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=sveltekit&tab=frameworks):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=sveltekit">
|
||||
@@ -41,6 +49,8 @@ PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Create the Supabase client
|
||||
|
||||
Create a `src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client:
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a TanStack Start app
|
||||
## 3. Create a TanStack Start app
|
||||
|
||||
Create a TanStack Start app using the official CLI.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a TanStack Start app using the official CLI.
|
||||
npm create @tanstack/start@latest my-app -- --package-manager npm --toolchain biome
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a TanStack Start app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the TanStack Start app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
|
||||
Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
|
||||
@@ -39,6 +47,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
## 7. Create a Supabase client utility
|
||||
|
||||
Create a new file at `src/utils/supabase.ts` to initialize the Supabase client.
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
|
||||
|
||||
<$Partial path="quickstart_db_setup.mdx" />
|
||||
|
||||
## 4. Create a Vue app
|
||||
## 3. Create a Vue app
|
||||
|
||||
Create a Vue app using the `npm init` command.
|
||||
|
||||
@@ -14,6 +14,16 @@ Create a Vue app using the `npm init` command.
|
||||
npm init vue@latest my-app
|
||||
```
|
||||
|
||||
## 4. Install Agent Skills (optional)
|
||||
|
||||
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
|
||||
|
||||
To install, run the following command in the root of your project:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
## 5. Install the Supabase client library
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Vue app.
|
||||
@@ -24,11 +34,9 @@ Navigate to the Vue app and install `supabase-js`.
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} />
|
||||
|
||||
## 6. Declare Supabase environment variables
|
||||
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=vue&tab=frameworks):
|
||||
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=vue&tab=frameworks):
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=vue">
|
||||
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
|
||||
```
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} />
|
||||
|
||||
## 7. Create the Supabase client
|
||||
|
||||
Create a `/src/lib` directory in your Vue app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client:
|
||||
|
||||
@@ -58,7 +58,7 @@ and run one of the following:
|
||||
- `sudo rpm -i <...>.rpm`
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="npm" label="nodejs">
|
||||
<TabPanel id="npm" label="Node.js">
|
||||
|
||||
Run the CLI by prefixing each command with `npx` or `bunx`:
|
||||
|
||||
@@ -122,7 +122,7 @@ brew link --overwrite supabase-beta
|
||||
Beta builds are attached to [GitHub pre-releases](https://github.com/supabase/cli/releases). Download the `.apk`, `.deb`, or `.rpm` for your platform and install with the same commands as [Linux packages](#linux-packages) above.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="npm" label="nodejs">
|
||||
<TabPanel id="npm" label="Node.js">
|
||||
|
||||
Install as a dev dependency:
|
||||
|
||||
@@ -199,7 +199,7 @@ brew upgrade supabase-beta
|
||||
- `sudo rpm -i <...>.rpm`
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="npm" label="nodejs">
|
||||
<TabPanel id="npm" label="Node.js">
|
||||
|
||||
If you have installed the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase), you can update it with:
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('your_project_url', 'your_supabase_api_key')
|
||||
|
||||
// ---cut---
|
||||
@@ -100,6 +101,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('your_project_url', 'your_supabase_api_key')
|
||||
|
||||
// ---cut---
|
||||
@@ -122,6 +124,7 @@ For passwordless signin via email or phone, check for users with verified email
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('your_project_url', 'your_supabase_api_key')
|
||||
|
||||
// ---cut---
|
||||
@@ -162,6 +165,7 @@ Both columns are accessible from the admin user methods. To create a user with c
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('your_project_url', 'your_supabase_api_key')
|
||||
|
||||
// ---cut---
|
||||
@@ -205,7 +209,7 @@ create table private.user_metadata (
|
||||
>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">I have IDs assigned to existing users in my database, how can I maintain these IDs?</span>}
|
||||
header="I have IDs assigned to existing users in my database, how can I maintain these IDs?"
|
||||
id="custom-user-id"
|
||||
>
|
||||
|
||||
@@ -228,7 +232,7 @@ const { data, error } = await supabase.auth.admin.createUser({
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">How can I allow my users to retain their existing password?</span>}
|
||||
header="How can I allow my users to retain their existing password?"
|
||||
id="existing-password"
|
||||
>
|
||||
|
||||
@@ -237,7 +241,7 @@ Supabase Auth never stores passwords as plaintext. Since Supabase Auth supports
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?</span>}
|
||||
header="My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?"
|
||||
id="mfa"
|
||||
>
|
||||
|
||||
@@ -246,7 +250,7 @@ You can obtain an export of your users' MFA secrets by opening a support ticket
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">How do I migrate existing SAML Single Sign-On (SSO) connections?</span>}
|
||||
header="How do I migrate existing SAML Single Sign-On (SSO) connections?"
|
||||
id="saml"
|
||||
>
|
||||
|
||||
@@ -255,7 +259,7 @@ Customers may need to link their identity provider with Supabase Auth separately
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">How do I migrate my Auth0 organizations to Supabase?</span>}
|
||||
header="How do I migrate my Auth0 organizations to Supabase?"
|
||||
id="migrate-org"
|
||||
>
|
||||
|
||||
|
||||
@@ -72,7 +72,12 @@ print('Analytics bucket created:', response)
|
||||
4. Select **Analytics Bucket** as the bucket type.
|
||||
5. Click **Create**.
|
||||
|
||||
<img alt="Create Analytics Bucket in Dashboard" src="/docs/img/storage/iceberg-bucket.png" />
|
||||
<Image
|
||||
alt="Create Analytics Bucket in Dashboard"
|
||||
width="644"
|
||||
height="402"
|
||||
src="/docs/img/storage/iceberg-bucket.png"
|
||||
/>
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -23,7 +23,12 @@ To authenticate with S3, generate a pair of credentials (Access Key ID and Secre
|
||||
|
||||
This is all the information you need to connect to Supabase Storage using any S3-compatible service.
|
||||
|
||||
<img alt="Storage S3 Access keys" src="/docs/img/storage/s3-credentials.png" width="100%" />
|
||||
<Image
|
||||
alt="Storage S3 Access keys"
|
||||
src="/docs/img/storage/s3-credentials.png"
|
||||
width="749"
|
||||
height="490"
|
||||
/>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
|
||||
@@ -27,10 +27,11 @@ Pick the workflow that best matches your tooling. Cards link to Supabase-authore
|
||||
|
||||
<MetricsStackCards />
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
|
||||
alt="Supabase Grafana dashboard showcasing database metrics"
|
||||
className="mt-8 rounded-lg border border-foreground/10 shadow-xs"
|
||||
width="3000"
|
||||
height="1509"
|
||||
/>
|
||||
|
||||
## Additional resources
|
||||
|
||||
@@ -44,10 +44,11 @@ If you prefer to reuse an existing Grafana Agent deployment, configure an [integ
|
||||
|
||||
This dashboard includes 200+ charts grouped by CPU, IO, connections, replication, WAL, and bloat indicators.
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
|
||||
alt="Supabase Grafana dashboard showcasing database metrics"
|
||||
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
|
||||
width="3000"
|
||||
height="1509"
|
||||
/>
|
||||
|
||||
## 4. Configure alerts (optional)
|
||||
|
||||
@@ -63,10 +63,11 @@ Install Grafana (Docker image, Helm chart, or packages) and connect it to Promet
|
||||
|
||||
You now have over 200 production-ready panels covering CPU, IO, WAL, replication, index bloat, and query throughput.
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
|
||||
alt="Supabase Grafana dashboard showcasing database metrics"
|
||||
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
|
||||
width="3000"
|
||||
height="1509"
|
||||
/>
|
||||
|
||||
## 4. Configure alerting
|
||||
|
||||
@@ -51,10 +51,11 @@ No matter which collector you use, you need to hit the Metrics API once per minu
|
||||
- For other tools, group metrics by categories (CPU, IO, WAL, replication, connections) and recreate the visualizations that matter most to your team.
|
||||
- Tag or relabel series with `project`, `env`, or `team` labels to make multi-project views easier.
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
|
||||
alt="Supabase Grafana dashboard showcasing database metrics"
|
||||
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
|
||||
width="3000"
|
||||
height="1509"
|
||||
/>
|
||||
|
||||
## 4. Alerts and automation
|
||||
|
||||
@@ -3,121 +3,82 @@ title: 'Sentry integration'
|
||||
subtitle: 'Integrate Sentry to monitor errors from a Supabase client'
|
||||
---
|
||||
|
||||
You can use [Sentry](https://sentry.io/welcome/) to monitor errors thrown from a Supabase JavaScript client. Install the [Supabase Sentry integration](https://github.com/supabase-community/sentry-integration-js) to get started.
|
||||
You can use [Sentry](https://sentry.io/welcome/) to monitor errors thrown from a Supabase JavaScript client. Support for Supabase is built directly into the Sentry JavaScript SDK.
|
||||
|
||||
The Sentry integration supports browser, Node, and edge environments.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the Sentry integration using your package manager:
|
||||
|
||||
<Tabs scrollable queryGroup="package-manager" defaultActiveId="npm" type="underlined" size="small" >
|
||||
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
```sh
|
||||
npm install @supabase/sentry-js-integration
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="yarn" label="yarn">
|
||||
|
||||
```sh
|
||||
yarn add @supabase/sentry-js-integration
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="pnpm" label="pnpm">
|
||||
|
||||
```sh
|
||||
pnpm add @supabase/sentry-js-integration
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Use
|
||||
The integration instruments database queries and authentication calls made through `supabase-js`, creating spans for performance monitoring and capturing errors. It supports browser, Node, and edge environments.
|
||||
|
||||
<Admonition type='note'>
|
||||
|
||||
If you are using Sentry JavaScript SDK v7, reference [`supabase-community/sentry-integration-js` repository](https://github.com/supabase-community/sentry-integration-js/blob/master/README-v7.md) instead.
|
||||
The built-in integration requires Sentry JavaScript SDK **v9.14.0 or later**. If you're on an older SDK (including v7), use the community [`@supabase/sentry-js-integration`](https://github.com/supabase-community/sentry-integration-js) package instead, which the built-in integration is based on.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To use the Supabase Sentry integration, add it to your `integrations` list when initializing your Sentry client.
|
||||
## Use
|
||||
|
||||
You can supply either the Supabase Client constructor or an already-initiated instance of a Supabase Client.
|
||||
There are two ways to enable the integration. Both take an initialized Supabase client instance.
|
||||
|
||||
<Tabs scrollable defaultActiveId="constructor" type="underlined" size="small">
|
||||
<Tabs scrollable defaultActiveId="integration" type="underlined" size="small">
|
||||
|
||||
<TabPanel id="constructor" label="With constructor">
|
||||
<TabPanel id="integration" label="Via Sentry.init">
|
||||
|
||||
```ts
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
integrations: [
|
||||
supabaseIntegration(SupabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="instance" label="With instance">
|
||||
Add `supabaseIntegration` to the `integrations` list when you initialize Sentry. Use this when your `Sentry.init` call and your Supabase client live in the same place.
|
||||
|
||||
```ts
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
integrations: [
|
||||
supabaseIntegration(supabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.supabaseIntegration({ supabaseClient }),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
<TabPanel id="instrument" label="Via instrumentSupabaseClient">
|
||||
|
||||
All available configuration options are available in our [`supabase-community/sentry-integration-js` repository](https://github.com/supabase-community/sentry-integration-js/blob/master/README.md#options).
|
||||
|
||||
## Deduplicating spans
|
||||
|
||||
If you're already monitoring HTTP errors in Sentry, for example with the HTTP, Fetch, or Undici integrations, you will get duplicate spans for Supabase calls. You can deduplicate the spans by skipping them in your other integration:
|
||||
Call `Sentry.instrumentSupabaseClient` where you create the client. This is the better fit for frameworks (like Next.js) where `Sentry.init` runs in a separate config file. Instrument each client you create: it patches database calls per runtime (browser, server, edge) and auth calls per client instance, so setups that create a client per request (like `@supabase/ssr`) must instrument each one. See the Next.js example below.
|
||||
|
||||
```ts
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
Sentry.instrumentSupabaseClient(supabaseClient)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<Admonition type='note'>
|
||||
|
||||
By default, query filters and mutation bodies are redacted from spans and breadcrumbs. To capture them, pass `sendOperationData: true` where you set up instrumentation (`Sentry.supabaseIntegration({ supabaseClient, sendOperationData: true })` or `Sentry.instrumentSupabaseClient(client, { sendOperationData: true })`), or enable `dataCollection: { userInfo: true }` in your `Sentry.init` options, which applies to every client in that runtime.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Deduplicating spans
|
||||
|
||||
Sentry's HTTP and Fetch tracing integrations are enabled by default in the Node and Next.js SDKs, so the underlying Supabase REST calls are traced as `http.client` spans in addition to the `db` spans from the Supabase integration. This is optional cleanup: if you'd rather not see both, skip the Supabase REST requests in your other integration.
|
||||
|
||||
```ts
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
integrations: [
|
||||
supabaseIntegration(SupabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
Sentry.supabaseIntegration({ supabaseClient }),
|
||||
|
||||
// @sentry/browser
|
||||
Sentry.browserTracingIntegration({
|
||||
@@ -126,23 +87,14 @@ Sentry.init({
|
||||
},
|
||||
}),
|
||||
|
||||
// or @sentry/node
|
||||
Sentry.httpIntegration({
|
||||
tracing: {
|
||||
ignoreOutgoingRequests: (url) => {
|
||||
return url.startsWith(`${SUPABASE_URL}/rest`)
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// or @sentry/node with Fetch support
|
||||
// or @sentry/node (supabase-js uses fetch, so filter the Fetch integration)
|
||||
Sentry.nativeNodeFetchIntegration({
|
||||
ignoreOutgoingRequests: (url) => {
|
||||
return url.startsWith(`${SUPABASE_URL}/rest`)
|
||||
},
|
||||
}),
|
||||
|
||||
// or @sentry/WinterCGFetch for Next.js Proxy & Edge Functions
|
||||
// or @sentry/nextjs for Proxy & Edge Functions
|
||||
Sentry.winterCGFetchIntegration({
|
||||
breadcrumbs: true,
|
||||
shouldCreateSpanForRequest: (url) => {
|
||||
@@ -153,125 +105,28 @@ Sentry.init({
|
||||
})
|
||||
```
|
||||
|
||||
## Example Next.js configuration
|
||||
## Configuration for Next.js
|
||||
|
||||
See this example for a setup with Next.js to cover browser, server, and edge environments. First, run through the [Sentry Next.js wizard](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) to generate the base Next.js configuration. Then add the Supabase Sentry Integration to all your `Sentry.init` calls with the appropriate filters.
|
||||
Next.js runs Sentry across browser, server, and edge runtimes, and auth-aware setups (like `@supabase/ssr`) create a Supabase client per request. Since `instrumentSupabaseClient` patches database calls per runtime and auth calls per client instance, call it inside each of your client factories rather than on a single shared instance.
|
||||
|
||||
<Tabs scrollable defaultActiveId="browser" type="underlined" size="small">
|
||||
1. Run through the [Sentry Next.js wizard](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) to set up the base Sentry configuration.
|
||||
|
||||
<TabPanel id="browser" label="Browser">
|
||||
2. Add `Sentry.instrumentSupabaseClient` to each factory. For example, the server client with `@supabase/ssr`:
|
||||
|
||||
```ts sentry.client.config.ts
|
||||
```ts utils/supabase/server.ts
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
integrations: [
|
||||
supabaseIntegration(SupabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
Sentry.browserTracingIntegration({
|
||||
shouldCreateSpanForRequest: (url) => {
|
||||
return !url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
|
||||
},
|
||||
}),
|
||||
],
|
||||
export async function createClient() {
|
||||
const client = createServerClient(/* your usual URL, key, and cookie config */)
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: true,
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="server" label="Server">
|
||||
|
||||
```ts sentry.server.config.ts
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
integrations: [
|
||||
supabaseIntegration(SupabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
Sentry.nativeNodeFetchIntegration({
|
||||
breadcrumbs: true,
|
||||
ignoreOutgoingRequests: (url) => {
|
||||
return url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: true,
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="edge" label="Proxy & Edge">
|
||||
|
||||
```js sentry.edge.config.ts
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { supabaseIntegration } from '@supabase/sentry-js-integration'
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
integrations: [
|
||||
supabaseIntegration(SupabaseClient, Sentry, {
|
||||
tracing: true,
|
||||
breadcrumbs: true,
|
||||
errors: true,
|
||||
}),
|
||||
Sentry.winterCGFetchIntegration({
|
||||
breadcrumbs: true,
|
||||
shouldCreateSpanForRequest: (url) => {
|
||||
return !url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: true,
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="instrumentation" label="Instrumentation">
|
||||
|
||||
```js instrumentation.ts
|
||||
// https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config')
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config')
|
||||
}
|
||||
Sentry.instrumentSupabaseClient(client)
|
||||
return client
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
3. Apply the same in your browser client using (`createBrowserClient`) and middleware client so every runtime is covered.
|
||||
|
||||
</Tabs>
|
||||
4. To include query filters and mutation bodies, enable `dataCollection: { userInfo: true }` in each runtime's Sentry config, or pass `Sentry.instrumentSupabaseClient(client, { sendOperationData: true })` at the call site.
|
||||
|
||||
Afterwards, build your application (`npm run build`) and start it locally (`npm run start`). You will now see the transactions being logged in the terminal when making supabase-js requests.
|
||||
5. Build and run your application (`npm run build && npm run start`). Supabase queries now appear as `db` spans in your Sentry traces.
|
||||
@@ -102,9 +102,9 @@ const GuideTemplate = ({
|
||||
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
|
||||
</h1>
|
||||
{meta?.subtitle && (
|
||||
<h2 className="mt-1 text-xl text-foreground-light [&>p]:m-0">
|
||||
<div className="mt-3 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
|
||||
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
@@ -121,7 +121,7 @@ const GuideTemplate = ({
|
||||
className={cn(
|
||||
'w-fit',
|
||||
'flex items-center gap-1',
|
||||
'text-sm text-scale-1000 hover:text-scale-1200',
|
||||
'text-sm text-tertiary-foreground hover:text-foreground',
|
||||
'transition-colors'
|
||||
)}
|
||||
target="_blank"
|
||||
|
||||
@@ -17,8 +17,7 @@ export function GuideFooter({ className, editLink }: GuideFooterProps) {
|
||||
className={cn(
|
||||
'w-fit',
|
||||
'flex items-center gap-1',
|
||||
'text-sm text-scale-1000 hover:text-scale-1200',
|
||||
'transition-colors'
|
||||
'text-sm text-tertiary-foreground hover:text-foreground'
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener edit"
|
||||
|
||||
@@ -18,7 +18,7 @@ export function GuideHeader({ className }: GuideHeaderProps) {
|
||||
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
|
||||
</h1>
|
||||
{meta?.subtitle && (
|
||||
<div className="mt-6 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
|
||||
<div className="mt-3 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
|
||||
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,12 +13,15 @@ import { parse as parseToml } from 'smol-toml'
|
||||
import { mcpConfigPanelMarkdown as McpConfigPanel } from 'ui-patterns/McpUrlBuilder/McpConfigPanel.md'
|
||||
|
||||
import { addBaseUrlPrefix } from './internal-links'
|
||||
import { AccordionItem } from './markdown-schema/Accordion'
|
||||
import { Admonition } from './markdown-schema/Admonition'
|
||||
import { AuthProviders } from './markdown-schema/AuthProviders'
|
||||
import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable'
|
||||
import { ContentListings } from './markdown-schema/ContentListings'
|
||||
import { CustomContent } from './markdown-schema/CustomContent'
|
||||
import { ErrorCodes } from './markdown-schema/ErrorCodes'
|
||||
import { IconCheck, IconX } from './markdown-schema/Icons'
|
||||
import { Image } from './markdown-schema/Image'
|
||||
import { Link } from './markdown-schema/Link'
|
||||
import { McpCiConfigBlock } from './markdown-schema/McpCiConfigBlock'
|
||||
import { MetricsStackCards } from './markdown-schema/MetricsStackCards'
|
||||
@@ -151,7 +154,11 @@ function applySchema(parent: Parent, schema: ComponentSchema): void {
|
||||
* component not listed is unwrapped (children are kept, wrapper is dropped).
|
||||
*/
|
||||
const SCHEMA: ComponentSchema = {
|
||||
AccordionItem,
|
||||
Admonition,
|
||||
IconCheck,
|
||||
IconX,
|
||||
Image,
|
||||
AuthProviders,
|
||||
ComputeDiskLimitsTable,
|
||||
CustomContent,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const AccordionItem = ({
|
||||
props,
|
||||
children,
|
||||
}: {
|
||||
props: Record<string, unknown>
|
||||
children: string
|
||||
}): string => {
|
||||
const header = String(props.header ?? '').trim()
|
||||
return header ? `**${header}**\n\n${children}` : children
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const IconCheck = (): string => '✅'
|
||||
export const IconX = (): string => '❌'
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getInternalLinkBaseUrl, withDocsBasePath } from '../internal-links'
|
||||
|
||||
export const Image = ({ props }: { props: Record<string, unknown> }): string => {
|
||||
const alt = String(props.alt ?? '')
|
||||
const rawSrc = props.src as string
|
||||
|
||||
// propsFrom() always stores attribute values as strings: either the literal
|
||||
// string value for `src="/..."`, or the raw JS expression for `src={{ dark, light }}`.
|
||||
const srcStr = String(rawSrc ?? '')
|
||||
let src = String(props.src)
|
||||
|
||||
if (!srcStr.startsWith('/')) {
|
||||
// Object expression form: `src={{ dark: '/...', light: '/...' }}`
|
||||
const match =
|
||||
srcStr.match(/dark:\s*['"]([^'"]+)['"]/) ?? srcStr.match(/light:\s*['"]([^'"]+)['"]/)
|
||||
src = match ? match[1] : ''
|
||||
}
|
||||
|
||||
const baseUrl = getInternalLinkBaseUrl()
|
||||
return `})`
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
export const Panel = ({ props }: { props: Record<string, unknown> }): string => {
|
||||
return String(props.title)
|
||||
export const Panel = ({
|
||||
props,
|
||||
children,
|
||||
}: {
|
||||
props: Record<string, unknown>
|
||||
children: string
|
||||
}): string => {
|
||||
const title = String(props.title ?? '')
|
||||
const description = children.replace(/\s+/g, ' ').trim()
|
||||
|
||||
if (title && description) return `${title}: ${description}`
|
||||
|
||||
return title || description
|
||||
}
|
||||
@@ -181,6 +181,7 @@
|
||||
"tsconfig": "workspace:*",
|
||||
"tsx": "catalog:",
|
||||
"twoslash": "^0.3.1",
|
||||
"@typescript/native": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unist-util-visit-parents": "5.1.3",
|
||||
"vite": "catalog:",
|
||||
|
||||
@@ -264,6 +264,7 @@ Sean Romberg
|
||||
Sean Thompson
|
||||
Sergio Cioban Filho
|
||||
Shaii O
|
||||
Shane Adams
|
||||
Shane E
|
||||
Shreekar Shetty
|
||||
Sreyas Udayavarman
|
||||
|
||||
@@ -2589,11 +2589,16 @@ functions:
|
||||
notes: |
|
||||
Delete a user.
|
||||
- The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column.
|
||||
- When `shouldSoftDelete` is `true`, the user is soft-deleted: their record and associated data are retained but the user is marked as deleted. Defaults to `false`, which permanently removes the user.
|
||||
params:
|
||||
- name: id
|
||||
isOptional: false
|
||||
type: String
|
||||
description: ID of the user to be deleted.
|
||||
- name: shouldSoftDelete
|
||||
isOptional: true
|
||||
type: bool
|
||||
description: If true, soft-deletes the user (keeps the record but marks it deleted). Defaults to false (permanent delete).
|
||||
examples:
|
||||
- id: removes-a-user
|
||||
name: Removes a user
|
||||
@@ -2603,6 +2608,16 @@ functions:
|
||||
await supabase.auth.admin
|
||||
.deleteUser('715ed5db-f090-4b8c-a067-640ecee36aa0');
|
||||
```
|
||||
- id: soft-delete-a-user
|
||||
name: Soft-delete a user
|
||||
code: |
|
||||
```dart
|
||||
await supabase.auth.admin
|
||||
.deleteUser(
|
||||
'715ed5db-f090-4b8c-a067-640ecee36aa0',
|
||||
shouldSoftDelete: true,
|
||||
);
|
||||
```
|
||||
- id: invite-user-by-email
|
||||
title: 'inviteUserByEmail()'
|
||||
notes: |
|
||||
@@ -4634,6 +4649,10 @@ functions:
|
||||
isOptional: false
|
||||
type: int
|
||||
description: The number of seconds until the signed URL expires. For example, 60 for a URL which is valid for one minute.
|
||||
- name: download
|
||||
isOptional: true
|
||||
type: DownloadBehavior
|
||||
description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it.
|
||||
- name: transform
|
||||
isOptional: true
|
||||
type: TransformOptions
|
||||
@@ -4690,6 +4709,61 @@ functions:
|
||||
```json
|
||||
'https://example.supabase.co/storage/v1/object/sign/avatars/folder/avatar1.png?token=<TOKEN>''
|
||||
```
|
||||
- id: create-signed-url-with-download
|
||||
name: With download
|
||||
code: |
|
||||
```dart
|
||||
final String signedUrl = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.createSignedUrl(
|
||||
'avatar1.png',
|
||||
60,
|
||||
download: DownloadBehavior.withOriginalName,
|
||||
);
|
||||
```
|
||||
|
||||
- id: from-create-signed-upload-url
|
||||
description: |
|
||||
Creates a signed upload URL. Signed upload URLs can be used to upload files to a bucket without further authentication. They are valid for 2 hours.
|
||||
title: 'from.createSignedUploadUrl()'
|
||||
notes: |
|
||||
- Policy permissions required:
|
||||
- `buckets` permissions: none
|
||||
- `objects` permissions: `insert`
|
||||
- Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works
|
||||
params:
|
||||
- name: path
|
||||
isOptional: false
|
||||
type: String
|
||||
description: The file path, including the current file name. For example folder/image.png.
|
||||
- name: upsert
|
||||
isOptional: true
|
||||
type: bool
|
||||
description: If true, the signed URL allows overwriting an existing file at the path. Defaults to false.
|
||||
examples:
|
||||
- id: create-signed-upload-url
|
||||
name: Create signed upload URL
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```dart
|
||||
final response = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.createSignedUploadUrl('folder/avatar1.png');
|
||||
```
|
||||
- id: create-signed-upload-url-with-upsert
|
||||
name: With upsert
|
||||
code: |
|
||||
```dart
|
||||
final response = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.createSignedUploadUrl(
|
||||
'folder/avatar1.png',
|
||||
upsert: true,
|
||||
);
|
||||
```
|
||||
|
||||
- id: from-get-public-url
|
||||
description: |
|
||||
@@ -4706,6 +4780,10 @@ functions:
|
||||
isOptional: false
|
||||
type: String
|
||||
description: The path and name of the file to generate the public URL for. For example folder/image.png.
|
||||
- name: download
|
||||
isOptional: true
|
||||
type: DownloadBehavior
|
||||
description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it.
|
||||
- name: transform
|
||||
isOptional: true
|
||||
type: TransformOptions
|
||||
@@ -4766,6 +4844,18 @@ functions:
|
||||
```json
|
||||
'https://example.supabase.co/storage/v1/object/public/public-bucket/folder/avatar1.png'
|
||||
```
|
||||
- id: returns-the-url-for-an-asset-with-download
|
||||
name: Trigger download
|
||||
code: |
|
||||
```dart
|
||||
final String publicUrl = supabase
|
||||
.storage
|
||||
.from('public-bucket')
|
||||
.getPublicUrl(
|
||||
'avatar1.png',
|
||||
download: DownloadBehavior.withOriginalName,
|
||||
);
|
||||
```
|
||||
|
||||
- id: from-download
|
||||
description: |
|
||||
|
||||
@@ -4384,6 +4384,8 @@ functions:
|
||||
It's best to only enable this for testing environments but if you wish to enable it for production you can provide additional protection by using a `pre-request` function.
|
||||
|
||||
Follow the [Performance Debugging Guide](/docs/guides/database/debugging-performance) to enable the functionality on your project.
|
||||
notes: |
|
||||
The `format` parameter accepts an `ExplainFormat` value — either `.text` (default, human-readable) or `.json` (machine-readable JSON).
|
||||
examples:
|
||||
- id: get-execution-plan
|
||||
name: Get the execution plan
|
||||
@@ -4463,6 +4465,21 @@ functions:
|
||||
By default, the data is returned in TEXT format, but can also be returned as JSON by using the `format` parameter.
|
||||
hideCodeBlock: true
|
||||
isSpotlight: false
|
||||
|
||||
- id: get-execution-plan-json-format
|
||||
name: Get the execution plan as JSON
|
||||
code: |
|
||||
```swift
|
||||
try await supabase
|
||||
.from("instruments")
|
||||
.select()
|
||||
.explain(format: .json)
|
||||
.execute()
|
||||
.value
|
||||
```
|
||||
description: |
|
||||
Use `format: .json` to get a machine-readable JSON execution plan. The `ExplainFormat` type accepts `.text` (default) or `.json`.
|
||||
isSpotlight: false
|
||||
- id: invoke
|
||||
title: invoke()
|
||||
description: |
|
||||
@@ -5176,7 +5193,7 @@ functions:
|
||||
.createBucket(
|
||||
"avatars",
|
||||
options: BucketOptions(
|
||||
public: false,
|
||||
isPublic: false,
|
||||
allowedMimeTypes: ["image/png"],
|
||||
fileSizeLimit: 1024
|
||||
)
|
||||
@@ -5216,7 +5233,7 @@ functions:
|
||||
.updateBucket(
|
||||
"avatars",
|
||||
options: BucketOptions(
|
||||
public: false,
|
||||
isPublic: false,
|
||||
fileSizeLimit: 1024,
|
||||
allowedMimeTypes: ["image/png"]
|
||||
)
|
||||
@@ -5375,12 +5392,13 @@ functions:
|
||||
let signedURL = try await supabase.storage
|
||||
.from("avatars")
|
||||
.createSignedURL(
|
||||
path: "folder/avatar1.png", expiresIn: 60,
|
||||
download: true
|
||||
path: "folder/avatar1.png",
|
||||
expiresIn: 60,
|
||||
download: .withOriginalName
|
||||
)
|
||||
```
|
||||
note: |
|
||||
You can also sepcify a `String` in the `download` parameter to define the file name for the downloaded asset.
|
||||
Use `.withOriginalName` to download with the original file name, or `.named("custom.png")` to specify a custom file name. The `download: Bool` overload still compiles but is disfavored.
|
||||
|
||||
- id: from-create-signed-urls
|
||||
title: from.createSignedUrls()
|
||||
@@ -5519,11 +5537,11 @@ functions:
|
||||
.from("public-bucket")
|
||||
.getPublicURL(
|
||||
path: "folder/avatar1.png",
|
||||
download: true
|
||||
download: .withOriginalName
|
||||
)
|
||||
```
|
||||
note: |
|
||||
You can also sepcify a `String` in the `download` parameter to define the file name for the downloaded asset.
|
||||
Use `.withOriginalName` to download with the original file name, or `.named("custom.png")` to specify a custom file name. The `download: Bool` overload still compiles but is disfavored.
|
||||
|
||||
- id: from-download
|
||||
title: from.download()
|
||||
|
||||
@@ -424,7 +424,7 @@ th code {
|
||||
--code-foreground: #ffffff;
|
||||
--code-token-constant: #3ecf8e;
|
||||
--code-token-string: #ffcda1;
|
||||
--code-token-comment: #7e7e7e;
|
||||
--code-token-comment: #949494;
|
||||
--code-token-parameter: #ffffff;
|
||||
--code-token-function: #3ecf8e;
|
||||
--code-token-string-expression: #ffcda1;
|
||||
@@ -436,14 +436,14 @@ th code {
|
||||
}
|
||||
[data-theme='light'],
|
||||
.light {
|
||||
--code-token-keyword: #6b35dc;
|
||||
--code-token-keyword: #5f2fc4;
|
||||
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-constant: #15593b;
|
||||
--code-token-string: #f1a10d;
|
||||
--code-token-comment: #7e7e7e;
|
||||
--code-token-string: #9a5200;
|
||||
--code-token-comment: #6a6a6a;
|
||||
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-function: #15593b;
|
||||
--code-token-string-expression: #f1a10d;
|
||||
--code-token-string-expression: #9a5200;
|
||||
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
--code-token-keyword: #6b35dc;
|
||||
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-constant: #15593b;
|
||||
--code-token-string: #f1a10d;
|
||||
--code-token-string: #c46a0a;
|
||||
--code-token-comment: #7e7e7e;
|
||||
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-function: #15593b;
|
||||
--code-token-string-expression: #f1a10d;
|
||||
--code-token-string-expression: #c46a0a;
|
||||
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"react-hooks/exhaustive-deps": 161,
|
||||
"import/no-anonymous-default-export": 57,
|
||||
"@tanstack/query/exhaustive-deps": 9,
|
||||
"@typescript-eslint/no-explicit-any": 898,
|
||||
"@typescript-eslint/no-explicit-any": 897,
|
||||
"no-restricted-imports": 0,
|
||||
"no-restricted-exports": 198,
|
||||
"react/no-unstable-nested-components": 38,
|
||||
@@ -556,7 +556,6 @@
|
||||
"hooks/misc/withAuth.tsx": 1,
|
||||
"hooks/ui/useClickedOutside.ts": 2,
|
||||
"hooks/ui/useFlag.ts": 1,
|
||||
"instrumentation-client.ts": 3,
|
||||
"lib/ai/generate-assistant-response.ts": 2,
|
||||
"lib/ai/model.ts": 2,
|
||||
"lib/ai/model.utils.ts": 3,
|
||||
@@ -571,6 +570,7 @@
|
||||
"lib/pg-format.ts": 6,
|
||||
"lib/profile.tsx": 1,
|
||||
"lib/role-impersonation.ts": 1,
|
||||
"lib/sentry-client-options.ts": 2,
|
||||
"lib/telemetry/track.ts": 1,
|
||||
"pages/_app.tsx": 1,
|
||||
"pages/api/ai/feedback/rate.ts": 2,
|
||||
|
||||
@@ -494,6 +494,80 @@ Keep this plugin even after migration — it's not a Next-related shim,
|
||||
it's general protection against this entire class of bug. Just clear
|
||||
the allowlist when the underlying cycle is gone.
|
||||
|
||||
### `@sentry/nextjs` → `@sentry/react` alias
|
||||
|
||||
`resolve.alias` in `vite.config.ts` rewrites the bare `@sentry/nextjs`
|
||||
import to `compat/sentry-nextjs.ts`, which re-exports `@sentry/react`
|
||||
(the same-version package `@sentry/nextjs` wraps on the client) plus
|
||||
explicit stand-ins for the Next-only APIs (`captureRouterTransitionStart`,
|
||||
`captureRequestError`, `withSentryConfig`).
|
||||
|
||||
Why: `@sentry/nextjs`'s client entry imports
|
||||
`next/dist/shared/lib/constants`, whose module scope evaluates
|
||||
`...(process?.features?.typescript ? ['next.config.mts'] : [])`.
|
||||
Optional chaining does **not** guard an undeclared `process` identifier,
|
||||
so every built client chunk containing it (table editor was the canary)
|
||||
threw `ReferenceError: process is not defined` at module load. Dev was
|
||||
unaffected (dev pipeline shims `process`), so it only surfaced in the
|
||||
production/test build.
|
||||
|
||||
The alias also made the previous `@sentry/nextjs` SSR workarounds
|
||||
(`ssr.noExternal` entry + `ssr.optimizeDeps.include`) obsolete — the id
|
||||
is rewritten before SSR resolution, and `@sentry/react` ships real ESM.
|
||||
App source keeps importing `@sentry/nextjs` so the Next build
|
||||
(`build:next`) is untouched; drop the alias + shim together with the
|
||||
Next build when the migration is done (switch imports to
|
||||
`@sentry/react` directly).
|
||||
|
||||
### GraphiQL Monaco workers: `setup-workers/webpack` → `setup-workers/vite`
|
||||
|
||||
App source (`GraphiQLTab.tsx`) imports `graphiql/setup-workers/webpack`,
|
||||
which registers `MonacoEnvironment.getWorker` using
|
||||
`new Worker(new URL('monaco-editor/...', import.meta.url))` — the URL form
|
||||
webpack/turbopack rewrites at build time. Vite doesn't rewrite bare module
|
||||
specifiers inside `new URL(..., import.meta.url)`, so under the TanStack
|
||||
build the worker URLs 404'd and Monaco fell back to running the `json`,
|
||||
`editorWorkerService` and `graphql` workers on the main thread ("Could not
|
||||
create web worker(s). Falling back to loading web worker code in main
|
||||
thread" in the console).
|
||||
|
||||
The `graphiqlViteWorkers` plugin in `vite.config.ts` resolves that import
|
||||
to graphiql's own `setup-workers/vite` variant (same three workers via
|
||||
Vite `?worker` imports) in client builds; SSR resolution is untouched. The
|
||||
import specifier stays `.../webpack` in app source so the Next build keeps
|
||||
working. The whole setup-workers chain is also in `optimizeDeps.exclude` —
|
||||
the Rolldown dep optimizer can't load `?worker` ids
|
||||
(`UNLOADABLE_DEPENDENCY`), so the modules go through the normal transform
|
||||
pipeline where Vite's built-in worker plugin handles them. Drop the plugin
|
||||
and the exclude, and switch the import to `graphiql/setup-workers/vite`,
|
||||
when the Next build goes away.
|
||||
|
||||
### Raw-text imports: `*.md` + `public/deno/*.d.ts` (`rawTextLoader`)
|
||||
|
||||
Next's raw-loader rules (next.config.ts `turbopack.rules`) serve `*.md`
|
||||
files and the Deno typings `public/deno/edge-runtime.d.ts` /
|
||||
`public/deno/lib.deno.d.ts` as JS modules whose default export is the
|
||||
file's text. The `rawTextLoader` plugin in `vite.config.ts` mirrors that
|
||||
for the Vite pipeline:
|
||||
|
||||
- `*.md` — plain `transform` (used by
|
||||
`static-data/integrations/*/overview.md` via the literal-import registry
|
||||
in `static-data/integrations/overviews.ts`).
|
||||
- The two Deno `.d.ts` files (used by `components/ui/AIEditor` as Monaco
|
||||
extra libs for edge-function editors) — an exact-specifier allowlist
|
||||
resolved to `\0`-virtual ids and served from a `load` hook. They can't go
|
||||
through `transform`: Rolldown's native dep scanner skips JS plugin hooks
|
||||
and hard-fails parsing TS _declaration_ syntax (`get stdin(): ...;`) as
|
||||
runtime TS, which killed dependency pre-bundling wholesale. The previous
|
||||
`/* @vite-ignore */` hack kept the scanner away but also meant the
|
||||
imports failed at runtime, silently dropping Deno type hints in the
|
||||
TanStack build. Do NOT widen the allowlist to `*.d.ts` — hijacking
|
||||
declaration-file resolution globally would corrupt packages that ship
|
||||
`.d.ts` next to their JS. The `as string` casts on the import specifiers
|
||||
in `AIEditor/index.tsx` keep tsc from resolving the `.d.ts` files as
|
||||
declaration files (TS2846) while erasing to plain literals both bundlers
|
||||
statically analyze.
|
||||
|
||||
### Other build-side migration changes
|
||||
|
||||
- `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
type Ref,
|
||||
} from 'react'
|
||||
|
||||
import { splitInternalUrl } from '@/lib/internal-url'
|
||||
|
||||
// Next's Link accepts either a string `href` or a `UrlObject`
|
||||
// ({pathname, query, hash}). Workspace source does both — flatten
|
||||
// `UrlObject` into `pathname?search#hash` first so the TanStack `to`
|
||||
@@ -62,92 +64,14 @@ function resolveHref(href: string | UrlObject): string {
|
||||
return `${pathname}${search}${hash}`
|
||||
}
|
||||
|
||||
// Inlined at build time via Vite's `define`. Must agree with Vite `base`
|
||||
// and `tanstackStart({ router: { basepath } })`. Empty string when no
|
||||
// basePath is configured. Used to strip a duplicate prefix in
|
||||
// `splitInternalUrl` below — see the comment there.
|
||||
const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
||||
|
||||
// TanStack Link's `to` prop is a route-pattern path; query params and hash
|
||||
// must be passed separately via `search` / `hash`. Studio code (and Next's
|
||||
// own contract) routinely passes one of three href shapes:
|
||||
// 1. a relative path like `/project/abc/editor/123?schema=public`
|
||||
// 2. a same-origin absolute URL produced by `new URL(...).toString()`,
|
||||
// e.g. `http://localhost:8082/project/abc/editor/123?schema=public`
|
||||
// (this is what `buildTableEditorUrl` does)
|
||||
// 3. a genuinely external URL like `https://supabase.com/docs`.
|
||||
//
|
||||
// If we forward any of these straight through to TanStack as `to`, TanStack
|
||||
// either fails to match a known route pattern (#1 with query) or treats
|
||||
// the whole thing as external (#2) and falls back to native browser
|
||||
// navigation — which the user sees as a full page reload.
|
||||
//
|
||||
// Split into three parts: pathname, search, hash. Same-origin absolute
|
||||
// URLs are normalised to a relative path. Cross-origin URLs are left
|
||||
// alone so TanStack's external-link path handles them.
|
||||
//
|
||||
// basePath quirk: TanStack's `to` is **basepath-relative** — given
|
||||
// `basepath: '/dashboard'` and `to: '/foo'`, TanStack builds the href
|
||||
// `/dashboard/foo`. Next's contract treats `href` as the **full path
|
||||
// from app root including basePath**, and studio code routinely
|
||||
// pre-prefixes BASE_PATH (e.g. `buildTableEditorUrl` calls
|
||||
// `new URL(`${BASE_PATH}/project/.../editor/...`, location.origin)`).
|
||||
// Forwarding the BASE_PATH-prefixed pathname as `to` makes TanStack
|
||||
// double-prefix it (`/dashboard/dashboard/project/...`). Strip the
|
||||
// basePath when we see it, so what we hand TanStack is always
|
||||
// basepath-relative.
|
||||
function splitInternalUrl(url: string): {
|
||||
to: string
|
||||
search?: Record<string, string>
|
||||
hash?: string
|
||||
} {
|
||||
// Try to detect cross-origin absolute URLs cheaply before paying for a
|
||||
// full parse. Protocol-relative URLs (`//host/...`) are always external.
|
||||
if (url.startsWith('//')) {
|
||||
return { to: url }
|
||||
}
|
||||
|
||||
// Use the document origin as the parse base so relative inputs resolve.
|
||||
// SSR has no `location`; fall back to a placeholder host that won't ever
|
||||
// collide with a real one.
|
||||
const base =
|
||||
typeof window !== 'undefined' && window.location ? window.location.origin : 'http://_/'
|
||||
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(url, base)
|
||||
} catch {
|
||||
return { to: url }
|
||||
}
|
||||
|
||||
// Cross-origin → leave for TanStack to handle as external.
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.location &&
|
||||
parsed.origin !== window.location.origin
|
||||
) {
|
||||
return { to: url }
|
||||
}
|
||||
|
||||
let pathname = parsed.pathname
|
||||
// Strip a leading basePath segment so we hand TanStack a basepath-
|
||||
// relative path. Match `/dashboard` exactly OR `/dashboard/...`; don't
|
||||
// strip a coincidental prefix like `/dashboard-other`.
|
||||
if (
|
||||
NEXT_PUBLIC_BASE_PATH &&
|
||||
(pathname === NEXT_PUBLIC_BASE_PATH || pathname.startsWith(`${NEXT_PUBLIC_BASE_PATH}/`))
|
||||
) {
|
||||
pathname = pathname.slice(NEXT_PUBLIC_BASE_PATH.length) || '/'
|
||||
}
|
||||
|
||||
const search = Object.fromEntries(parsed.searchParams)
|
||||
const hash = parsed.hash || undefined
|
||||
return {
|
||||
to: pathname,
|
||||
search: Object.keys(search).length > 0 ? search : undefined,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
// must be passed separately via `search` / `hash`. If we forward an href
|
||||
// straight through to TanStack as `to`, TanStack either mangles the query
|
||||
// (path interpolation percent-decodes it and strips control characters) or
|
||||
// treats a same-origin absolute URL as external and falls back to native
|
||||
// browser navigation — which the user sees as a full page reload. Splitting
|
||||
// (plus origin/basePath normalisation) lives in `splitInternalUrl`
|
||||
// (@/lib/internal-url), shared with the next/router shim.
|
||||
|
||||
// Next: prefetch=true|"auto" → eagerly preload; false → never; default
|
||||
// in production = true. TanStack's preload values are "intent" (on hover/
|
||||
|
||||
@@ -3,9 +3,12 @@ import {
|
||||
useMatches,
|
||||
useParams as useTanStackParams,
|
||||
useRouter as useTanStackRouter,
|
||||
type AnyRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { splitInternalUrl } from '@/lib/internal-url'
|
||||
|
||||
// `next/navigation` is the App Router hook surface (Next 13+). Studio is
|
||||
// still pages-based, so most of these are entry-points that arrive via
|
||||
// stray imports or shared packages — we keep the surface comprehensive
|
||||
@@ -29,13 +32,27 @@ export function useRouter() {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// Split query/hash out of the href — embedding them in TanStack's
|
||||
// `to` runs the whole string through path interpolation, which
|
||||
// percent-decodes it and strips control characters from query values
|
||||
// (see @/lib/internal-url). `search: {}` / `hash: ''` clear stale
|
||||
// state, matching Next's full-href navigation semantics.
|
||||
//
|
||||
// The `<AnyRouter, string>` type arguments opt out of the registered
|
||||
// route tree's strict typing: Next-style hrefs are free-form strings
|
||||
// that can't satisfy the route-path union at compile time.
|
||||
push: (href: string, _options?: NavigateOptions) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
router.navigate({ to: href as any })
|
||||
const { to, search, hash } = splitInternalUrl(href)
|
||||
router.navigate<AnyRouter, string>({ to, search: search ?? {}, hash: hash ?? '' })
|
||||
},
|
||||
replace: (href: string, _options?: NavigateOptions) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
router.navigate({ to: href as any, replace: true })
|
||||
const { to, search, hash } = splitInternalUrl(href)
|
||||
router.navigate<AnyRouter, string>({
|
||||
to,
|
||||
search: search ?? {},
|
||||
hash: hash ?? '',
|
||||
replace: true,
|
||||
})
|
||||
},
|
||||
// App Router's refresh() refetches the current route's data without
|
||||
// re-mounting the React tree. Closest TanStack equivalent is
|
||||
@@ -50,10 +67,14 @@ export function useRouter() {
|
||||
if (typeof window !== 'undefined') window.history.forward()
|
||||
},
|
||||
prefetch: (href: string, _options?: PrefetchOptions) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
router.preloadRoute({ to: href as any }).catch(() => {
|
||||
// Match Next's fire-and-forget contract.
|
||||
})
|
||||
const { to, search, hash } = splitInternalUrl(href)
|
||||
// `<string, string>` (TFrom, TTo) loosens `to` to a plain string for
|
||||
// the same free-form-href reason as `navigate` above.
|
||||
router
|
||||
.preloadRoute<string, string>({ to, search: search ?? {}, hash: hash ?? '' })
|
||||
.catch(() => {
|
||||
// Match Next's fire-and-forget contract.
|
||||
})
|
||||
},
|
||||
}),
|
||||
[router]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolveUrl } from './router'
|
||||
import { resolveSearchOrHashOnlyTarget, resolveUrl, withDefaultPathname } from './router'
|
||||
|
||||
describe('resolveUrl (next/router compat shim)', () => {
|
||||
it('returns string URLs untouched', () => {
|
||||
@@ -58,3 +58,79 @@ describe('resolveUrl (next/router compat shim)', () => {
|
||||
).toBe('/project/x?a=1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveSearchOrHashOnlyTarget (next/router compat shim)', () => {
|
||||
it('prefixes the current pathname on a ?-only target', () => {
|
||||
// The regression we fixed: `push({ query })` with no pathname produced a
|
||||
// relative `?...` that TanStack resolved by appending — landing on
|
||||
// `/advisors/security/?preset=...` (trailing slash injected).
|
||||
expect(resolveSearchOrHashOnlyTarget('?preset=x', '/advisors/security')).toBe(
|
||||
'/advisors/security?preset=x'
|
||||
)
|
||||
})
|
||||
|
||||
it('prefixes the current pathname on a #-only target', () => {
|
||||
expect(resolveSearchOrHashOnlyTarget('#invoices', '/org/slug/billing')).toBe(
|
||||
'/org/slug/billing#invoices'
|
||||
)
|
||||
})
|
||||
|
||||
it('strips a trailing slash from the current pathname, but keeps the root "/"', () => {
|
||||
expect(resolveSearchOrHashOnlyTarget('?a=1', '/auth/providers/')).toBe('/auth/providers?a=1')
|
||||
expect(resolveSearchOrHashOnlyTarget('?a=1', '/')).toBe('/?a=1')
|
||||
expect(resolveSearchOrHashOnlyTarget('?a=1', '')).toBe('/?a=1')
|
||||
})
|
||||
|
||||
it('leaves targets with a pathname untouched', () => {
|
||||
expect(resolveSearchOrHashOnlyTarget('/project/abc?x=1', '/elsewhere')).toBe('/project/abc?x=1')
|
||||
expect(resolveSearchOrHashOnlyTarget('/project/abc', '/elsewhere')).toBe('/project/abc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('withDefaultPathname (next/router compat shim)', () => {
|
||||
it('fills a missing pathname from the current route pattern so path params are consumed', () => {
|
||||
// The regression we fixed: `push({ query: { ...router.query, filter } })`
|
||||
// with no pathname leaked `ref`/`id` into the query string
|
||||
// (`/editor/17597/?ref=...&id=17597&filter=...`) because nothing
|
||||
// interpolated them back into the path.
|
||||
const target = withDefaultPathname(
|
||||
{ query: { ref: 'abc', id: '17597', filter: 'note:eq:hi' } },
|
||||
'/project/[ref]/editor/[id]',
|
||||
{ ref: 'abc', id: '17597' }
|
||||
)
|
||||
expect(resolveUrl(target)).toBe('/project/abc/editor/17597?filter=note%3Aeq%3Ahi')
|
||||
})
|
||||
|
||||
it('backfills params the caller omitted from the current route params', () => {
|
||||
const target = withDefaultPathname(
|
||||
{ query: { preset: 'WARN' } },
|
||||
'/project/[ref]/advisors/security',
|
||||
{ ref: 'abc' }
|
||||
)
|
||||
expect(resolveUrl(target)).toBe('/project/abc/advisors/security?preset=WARN')
|
||||
})
|
||||
|
||||
it('caller-provided query values win over backfilled params', () => {
|
||||
const target = withDefaultPathname({ query: { ref: 'other', page: '2' } }, '/project/[ref]', {
|
||||
ref: 'abc',
|
||||
})
|
||||
expect(resolveUrl(target)).toBe('/project/other?page=2')
|
||||
})
|
||||
|
||||
it('never backfills the TanStack _splat param', () => {
|
||||
const target = withDefaultPathname({ query: { a: '1' } }, '/org/_/[[...routeSlug]]', {
|
||||
_splat: 'x/y',
|
||||
})
|
||||
expect(resolveUrl(target)).toBe('/org/_/?a=1')
|
||||
})
|
||||
|
||||
it('leaves string URLs, explicit pathnames, and raw-string queries untouched', () => {
|
||||
expect(withDefaultPathname('/x?a=1', '/p/[ref]', { ref: 'r' })).toBe('/x?a=1')
|
||||
expect(
|
||||
withDefaultPathname({ pathname: '/y', query: { a: '1' } }, '/p/[ref]', { ref: 'r' })
|
||||
).toEqual({ pathname: '/y', query: { a: '1' } })
|
||||
expect(withDefaultPathname({ query: 'a=1' }, '/p/[ref]', { ref: 'r' })).toEqual({
|
||||
query: 'a=1',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
useParams,
|
||||
useSearch,
|
||||
useRouter as useTanStackRouter,
|
||||
type AnyRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { getRouterEventsProxy } from './_router-events'
|
||||
import { splitInternalUrl } from '@/lib/internal-url'
|
||||
|
||||
// Next's pages-router exposes `router.pathname` as the route *pattern*
|
||||
// (e.g. `/project/[ref]/sql/[id]`), not the resolved URL. TanStack's
|
||||
@@ -107,6 +109,45 @@ function interpolatePathname(
|
||||
return { pathname: interpolated, query: rest }
|
||||
}
|
||||
|
||||
// TanStack resolves a `?`- or `#`-only relative `to` by *appending* it to the
|
||||
// current path, injecting a trailing slash: navigating to `?preset=x` from
|
||||
// `/advisors/security` lands on `/advisors/security/?preset=x`. Next resolved
|
||||
// these against the current pathname. Prefix it explicitly (trailing slash
|
||||
// stripped; root stays `/`) so `push({ query })` with no pathname stays on
|
||||
// the exact current path.
|
||||
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
|
||||
export function resolveSearchOrHashOnlyTarget(to: string, currentPathname: string): string {
|
||||
if (!to.startsWith('?') && !to.startsWith('#')) return to
|
||||
let base = currentPathname || '/'
|
||||
if (base.length > 1 && base.endsWith('/')) base = base.slice(0, -1)
|
||||
return `${base}${to}`
|
||||
}
|
||||
|
||||
// Next resolves a pathname-less UrlObject against the *current route
|
||||
// pattern*, re-consuming dynamic params from `query` into the path — e.g.
|
||||
// `push({ query: { ...router.query, preset } })` on `/project/[ref]/advisors`
|
||||
// stays on `/project/<ref>/advisors?preset=…`. Because the shim's
|
||||
// `router.query` merges path params in (Next shape), skipping this would leak
|
||||
// `ref`/`id` into the query string. Params the caller didn't include are
|
||||
// backfilled from the current route's params (minus TanStack's `_splat`,
|
||||
// which no bracket segment can consume) so partial `{ query }` pushes stay
|
||||
// on-page instead of producing empty path segments.
|
||||
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
|
||||
export function withDefaultPathname(
|
||||
url: string | UrlObject,
|
||||
currentPathPattern: string,
|
||||
currentParams: Record<string, QueryValue>
|
||||
): string | UrlObject {
|
||||
if (typeof url === 'string' || url.pathname != null) return url
|
||||
if (!url.query || typeof url.query !== 'object') return url
|
||||
const { _splat, ...paramsWithoutSplat } = currentParams
|
||||
return {
|
||||
...url,
|
||||
pathname: currentPathPattern,
|
||||
query: { ...paramsWithoutSplat, ...url.query },
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
|
||||
export function resolveUrl(url: string | UrlObject): string {
|
||||
if (typeof url === 'string') return url
|
||||
@@ -135,8 +176,8 @@ export function resolveUrl(url: string | UrlObject): string {
|
||||
// `basepath: '/dashboard'` and `to: '/foo'`, it produces `/dashboard/foo`.
|
||||
// So we strip the origin AND the basePath when present; otherwise
|
||||
// `router.push('/dashboard/...')` would double-prefix to
|
||||
// `/dashboard/dashboard/...`. Mirrors the equivalent logic in the
|
||||
// next/link shim's `splitInternalUrl`. Cross-origin URLs pass through
|
||||
// `/dashboard/dashboard/...`. Mirrors the equivalent logic in
|
||||
// `splitInternalUrl` (@/lib/internal-url). Cross-origin URLs pass through
|
||||
// untouched so TanStack hands them to the browser as external.
|
||||
const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
||||
|
||||
@@ -231,9 +272,33 @@ export function useRouter() {
|
||||
// It's intentionally not part of Next's public TransitionOptions.
|
||||
options?: TransitionOptions & { _replace?: boolean }
|
||||
): Promise<boolean> => {
|
||||
const to = toRelativeSameOrigin(resolveUrl(url))
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await router.navigate({ to: to as any, replace: options?._replace })
|
||||
// `location.pathname` is already basepath-stripped by TanStack, so the
|
||||
// prefixed target stays basepath-relative like every other `to`.
|
||||
const target = resolveSearchOrHashOnlyTarget(
|
||||
// `useParams({ strict: false })` types as possibly-undefined; treat
|
||||
// "no params" as an empty record for backfilling.
|
||||
toRelativeSameOrigin(resolveUrl(withDefaultPathname(url, pathPattern, params ?? {}))),
|
||||
location.pathname
|
||||
)
|
||||
// Never embed `?query`/`#hash` inside TanStack's `to` — router-core
|
||||
// runs the whole string through path interpolation, which percent-
|
||||
// decodes it and strips control characters (dropping `%0A` newlines
|
||||
// from values like the Logs Explorer's `s` SQL param). Split into
|
||||
// { to, search, hash } instead; the target is already relative and
|
||||
// basepath-stripped, so splitInternalUrl's own origin/basePath
|
||||
// normalisation is a no-op here. `search: {}` clears the query,
|
||||
// matching Next's push-without-query semantics; same for `hash: ''`.
|
||||
//
|
||||
// The `<AnyRouter, string>` type arguments opt out of the registered
|
||||
// route tree's strict typing: Next-style hrefs are free-form strings
|
||||
// that can't satisfy the route-path union at compile time.
|
||||
const { to, search, hash } = splitInternalUrl(target)
|
||||
await router.navigate<AnyRouter, string>({
|
||||
to,
|
||||
search: search ?? {},
|
||||
hash: hash ?? '',
|
||||
replace: options?._replace,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -308,8 +373,16 @@ export function useRouter() {
|
||||
_options?: PrefetchOptions
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await router.preloadRoute({ to: url as any })
|
||||
// Split like navigate() above so a query string in the href
|
||||
// preloads the real target instead of a path-mangled one.
|
||||
// `<string, string>` (TFrom, TTo) loosens `to` to a plain string
|
||||
// for the same free-form-href reason as `navigate` above.
|
||||
const { to, search, hash } = splitInternalUrl(toRelativeSameOrigin(url))
|
||||
await router.preloadRoute<string, string>({
|
||||
to,
|
||||
search: search ?? {},
|
||||
hash: hash ?? '',
|
||||
})
|
||||
} catch {
|
||||
// Next's prefetch is fire-and-forget; swallow resolution errors
|
||||
// (e.g. unknown route) so callers don't have to guard.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* TanStack-build replacement for `@sentry/nextjs`.
|
||||
*
|
||||
* `@sentry/nextjs`'s client entry imports `next/dist/shared/lib/constants`,
|
||||
* which evaluates `...(process?.features?.typescript ? ['next.config.mts'] : [])`
|
||||
* at module scope. Optional chaining does NOT guard an undeclared `process`
|
||||
* identifier in the browser, so any built chunk containing it throws
|
||||
* `ReferenceError: process is not defined` at load time (observed on the
|
||||
* table editor route). The Vite dev server shims `process`, so the crash
|
||||
* only surfaces in the production/test build.
|
||||
*
|
||||
* vite.config.ts aliases `@sentry/nextjs` to this module for the TanStack
|
||||
* build only — app source keeps importing `@sentry/nextjs`, and the Next
|
||||
* build (`build:next`) is untouched.
|
||||
*
|
||||
* `@sentry/nextjs` re-exports `@sentry/react` (same version, 10.x) and adds
|
||||
* Next-specific helpers on top. Everything Studio uses at runtime —
|
||||
* `captureException`, `captureMessage`, `startSpan`, `withScope`, `setTag`,
|
||||
* `setUser`, `init`, and the `Event` / `Breadcrumb` / `StackFrame` types —
|
||||
* exists in `@sentry/react`, so re-export the lot.
|
||||
*/
|
||||
import { captureException } from '@sentry/react'
|
||||
|
||||
// eslint-disable-next-line barrel-files/avoid-re-export-all -- compat shim: its whole job is to mirror @sentry/react's surface
|
||||
export * from '@sentry/react'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Next-specific APIs with no `@sentry/react` equivalent. None of them are in
|
||||
// the TanStack/Vite module graph today (they're only referenced from
|
||||
// Next-convention files: instrumentation-client.ts, instrumentation.ts and
|
||||
// next.config.ts), but provide explicit stand-ins so the alias can never
|
||||
// produce an `undefined is not a function` crash if one leaks in later.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* `@sentry/nextjs`'s hook for Next's `onRouterTransitionStart` instrumentation
|
||||
* event. TanStack Router navigation spans would instead come from
|
||||
* `Sentry.tanstackRouterBrowserTracingIntegration` — no-op here.
|
||||
*/
|
||||
export function captureRouterTransitionStart(_href: string, _navigationType: string): void {}
|
||||
|
||||
/**
|
||||
* `@sentry/nextjs`'s hook for Next's `onRequestError` server instrumentation.
|
||||
* Report the error through the regular capture pipeline instead.
|
||||
*/
|
||||
export function captureRequestError(
|
||||
error: unknown,
|
||||
_request: unknown,
|
||||
_errorContext: unknown
|
||||
): void {
|
||||
// `captureException` from `@sentry/react` is isomorphic (no-op without an
|
||||
// initialized client), so this is safe on both server and client.
|
||||
captureException(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build-time-only wrapper for next.config — meaningless under Vite. Return
|
||||
* the config unchanged so any accidental usage is inert.
|
||||
*/
|
||||
export function withSentryConfig<T>(config: T, ..._options: unknown[]): T {
|
||||
return config
|
||||
}
|
||||
@@ -197,12 +197,14 @@ export function saveTableEditorStateToLocalStorage({
|
||||
gridColumns,
|
||||
sorts,
|
||||
filters,
|
||||
sensitiveDataColumns,
|
||||
}: {
|
||||
projectRef: string
|
||||
tableId: number
|
||||
gridColumns?: CalculatedColumn<any, any>[]
|
||||
sorts?: string[]
|
||||
filters?: string[]
|
||||
sensitiveDataColumns?: string[]
|
||||
}) {
|
||||
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
|
||||
const savedStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey)
|
||||
@@ -211,6 +213,7 @@ export function saveTableEditorStateToLocalStorage({
|
||||
...(gridColumns !== undefined && { gridColumns }),
|
||||
...(sorts !== undefined && { sorts: sorts.filter((sort) => sort !== '') }),
|
||||
...(filters !== undefined && { filters: filters.filter((filter) => filter !== '') }),
|
||||
...(sensitiveDataColumns !== undefined && { sensitiveDataColumns }),
|
||||
}
|
||||
|
||||
let savedJson
|
||||
@@ -287,6 +290,7 @@ export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
|
||||
rows: TRow[]
|
||||
columns: SupaColumn[]
|
||||
onRowsChange: (rows: TRow[], data: RowsChangeData<TRow, unknown>) => void
|
||||
sensitiveDataColumns?: Set<string>
|
||||
}
|
||||
) => {
|
||||
const { mode, column, row, rowIdx } = args
|
||||
@@ -296,11 +300,16 @@ export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
|
||||
if (key === 'c' && (event.metaKey || event.ctrlKey)) {
|
||||
if (window.getSelection()?.isCollapsed === false) return
|
||||
|
||||
const isSensitive = context?.sensitiveDataColumns?.has(column.key as string)
|
||||
const value = formatClipboardValue(row[column.key] ?? '')
|
||||
event.preventDefault()
|
||||
event.preventGridDefault()
|
||||
void copyToClipboard(value, () => {
|
||||
toast.success('Copied cell value to clipboard')
|
||||
if (isSensitive) {
|
||||
toast.warning('Copied sensitive data to clipboard')
|
||||
} else {
|
||||
toast.success('Copied cell value to clipboard')
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import type { RenderCellProps } from 'react-data-grid'
|
||||
|
||||
import { isColumnMasked } from '../../utils/sensitive-data'
|
||||
import { NullValue } from '../common/NullValue'
|
||||
import { SupaRow } from '@/components/grid/types'
|
||||
import { convertByteaToHex } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
|
||||
export const BinaryFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
const value = p.row[p.column.key]
|
||||
const isMasked = isColumnMasked(
|
||||
p.column.key as string,
|
||||
snap.sensitiveDataColumns,
|
||||
snap.temporarilyRevealedColumns
|
||||
)
|
||||
|
||||
if (!value) return <NullValue />
|
||||
if (isMasked) return <>••••••••</>
|
||||
const binaryValue = convertByteaToHex(value)
|
||||
return <>{binaryValue}</>
|
||||
}
|
||||
@@ -2,10 +2,20 @@ import type { PropsWithChildren } from 'react'
|
||||
import type { RenderCellProps } from 'react-data-grid'
|
||||
|
||||
import type { SupaRow } from '../../types'
|
||||
import { isColumnMasked } from '../../utils/sensitive-data'
|
||||
import { NullValue } from '../common/NullValue'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
|
||||
export const BooleanFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
const value = p.row[p.column.key] as boolean | null
|
||||
const isMasked = isColumnMasked(
|
||||
p.column.key as string,
|
||||
snap.sensitiveDataColumns,
|
||||
snap.temporarilyRevealedColumns
|
||||
)
|
||||
|
||||
if (value === null) return <NullValue />
|
||||
if (isMasked) return <>••••••••</>
|
||||
return <>{value ? 'TRUE' : 'FALSE'}</>
|
||||
}
|
||||
@@ -1,14 +1,26 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import type { RenderCellProps } from 'react-data-grid'
|
||||
|
||||
import { isColumnMasked } from '../../utils/sensitive-data'
|
||||
import { EmptyValue } from '../common/EmptyValue'
|
||||
import { NullValue } from '../common/NullValue'
|
||||
import { SupaRow } from '@/components/grid/types'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
|
||||
export const DefaultFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
let value = p.row[p.column.key]
|
||||
|
||||
// Check if column should be masked: marked sensitive AND not temporarily revealed
|
||||
const isMasked = isColumnMasked(
|
||||
p.column.key as string,
|
||||
snap.sensitiveDataColumns,
|
||||
snap.temporarilyRevealedColumns
|
||||
)
|
||||
|
||||
if (value === null) return <NullValue />
|
||||
if (value === '') return <EmptyValue />
|
||||
if (isMasked) return <>••••••••</>
|
||||
if (typeof value == 'object' || Array.isArray(value)) {
|
||||
value = JSON.stringify(value)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import type { SupaRow } from '../../types'
|
||||
import { isColumnMasked } from '../../utils/sensitive-data'
|
||||
import { NullValue } from '../common/NullValue'
|
||||
import { ReferenceRecordPeek } from './ReferenceRecordPeek'
|
||||
import { convertByteaToHex } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
|
||||
@@ -14,6 +15,7 @@ import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
|
||||
import { isTableLike } from '@/data/table-editor/table-editor-types'
|
||||
import { useTableQuery } from '@/data/tables/table-retrieve-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
|
||||
interface Props extends PropsWithChildren<RenderCellProps<SupaRow, unknown>> {
|
||||
tableId?: number
|
||||
@@ -21,7 +23,13 @@ interface Props extends PropsWithChildren<RenderCellProps<SupaRow, unknown>> {
|
||||
|
||||
export const ForeignKeyFormatter = (props: Props) => {
|
||||
const { tableId, row, column } = props
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const isMasked = isColumnMasked(
|
||||
column.key as string,
|
||||
snap.sensitiveDataColumns,
|
||||
snap.temporarilyRevealedColumns
|
||||
)
|
||||
|
||||
const { data, isPending: isLoading } = useTableEditorQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -58,7 +66,7 @@ export const ForeignKeyFormatter = (props: Props) => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between flex justify-between">
|
||||
<span className="m-0 grow overflow-hidden text-ellipsis">
|
||||
{formattedValue === null ? <NullValue /> : formattedValue}
|
||||
{formattedValue === null ? <NullValue /> : isMasked ? '••••••••' : formattedValue}
|
||||
</span>
|
||||
{isLoading && formattedValue !== null && (
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
|
||||
@@ -2,14 +2,23 @@ import { PropsWithChildren } from 'react'
|
||||
import type { RenderCellProps } from 'react-data-grid'
|
||||
|
||||
import type { SupaRow } from '../../types'
|
||||
import { isColumnMasked } from '../../utils/sensitive-data'
|
||||
import { EmptyValue } from '../common/EmptyValue'
|
||||
import { NullValue } from '../common/NullValue'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
|
||||
export const JsonFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
let value = p.row[p.column.key]
|
||||
const isMasked = isColumnMasked(
|
||||
p.column.key as string,
|
||||
snap.sensitiveDataColumns,
|
||||
snap.temporarilyRevealedColumns
|
||||
)
|
||||
|
||||
if (value === null) return <NullValue />
|
||||
if (value === '') return <EmptyValue />
|
||||
if (isMasked) return <>••••••••</>
|
||||
|
||||
// [Joshen] With reference to table-rows-query, we're only pulling max n characters on text/jsonb columns
|
||||
// If column value is longer, value will be concatenated with ..., and we just want to make sure the JSON
|
||||
|
||||
@@ -429,6 +429,7 @@ export const Grid = memo(
|
||||
rows: rows ?? [],
|
||||
columns: snap.table.columns,
|
||||
onRowsChange,
|
||||
sensitiveDataColumns: snap.sensitiveDataColumns,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ArrowDown, ArrowUp, ChevronDown, Copy, Edit, Lock, Trash, Unlock } from 'lucide-react'
|
||||
import { ArrowDown, ArrowUp, ChevronDown, Copy, Edit, Eye, Lock, Trash, Unlock } from 'lucide-react'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import type { CalculatedColumn } from 'react-data-grid'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
@@ -17,7 +19,10 @@ import {
|
||||
import { useTableSort } from '@/components/grid/hooks/useTableSort'
|
||||
import type { Sort } from '@/components/grid/types'
|
||||
import { useTableEditorStateSnapshot } from '@/state/table-editor'
|
||||
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
||||
import {
|
||||
TableEditorTableStateContext,
|
||||
useTableEditorTableStateSnapshot,
|
||||
} from '@/state/table-editor-table'
|
||||
|
||||
interface ColumnMenuProps {
|
||||
column: CalculatedColumn<any, unknown>
|
||||
@@ -27,7 +32,11 @@ interface ColumnMenuProps {
|
||||
export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
|
||||
const tableEditorSnap = useTableEditorStateSnapshot()
|
||||
const snap = useTableEditorTableStateSnapshot()
|
||||
const state = useContext(TableEditorTableStateContext)
|
||||
const { sorts, addOrUpdateSort, removeSort } = useTableSort()
|
||||
const [tempRevealTimeouts, setTempRevealTimeouts] = useState<Map<string, NodeJS.Timeout>>(
|
||||
new Map()
|
||||
)
|
||||
|
||||
const columnKey = column.key
|
||||
const columnName = column.name as string
|
||||
@@ -54,6 +63,50 @@ export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
tempRevealTimeouts.forEach((timeout) => clearTimeout(timeout))
|
||||
}
|
||||
}, [tempRevealTimeouts])
|
||||
|
||||
function onToggleSensitiveData() {
|
||||
const isMasked = snap.sensitiveDataColumns.has(columnKey)
|
||||
const isTemporarilyRevealed = snap.temporarilyRevealedColumns.has(columnKey)
|
||||
|
||||
if (isMasked && !isTemporarilyRevealed) {
|
||||
// Temporarily reveal for 5 seconds (don't persist)
|
||||
const existingTimeout = tempRevealTimeouts.get(columnKey)
|
||||
if (existingTimeout) clearTimeout(existingTimeout)
|
||||
|
||||
state.temporarilyRevealedColumns.add(columnKey)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
state.temporarilyRevealedColumns.delete(columnKey)
|
||||
setTempRevealTimeouts((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.delete(columnKey)
|
||||
return next
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
setTempRevealTimeouts((prev) => new Map(prev).set(columnKey, timeout))
|
||||
toast.info('Data will be hidden again in 5 seconds')
|
||||
} else {
|
||||
// Column is being revealed (either temp or persistent), toggle persistent mask
|
||||
const existingTimeout = tempRevealTimeouts.get(columnKey)
|
||||
if (existingTimeout) clearTimeout(existingTimeout)
|
||||
|
||||
state.toggleSensitiveDataColumn(columnKey)
|
||||
|
||||
state.temporarilyRevealedColumns.delete(columnKey)
|
||||
setTempRevealTimeouts((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.delete(columnKey)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onSortColumn(ascending: boolean) {
|
||||
if (!columnKey) return
|
||||
const currentSort = sorts.find((s) => s.column === columnKey)
|
||||
@@ -140,6 +193,18 @@ export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
{snap.sensitiveDataColumns.has(columnKey) && (
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
disabled={snap.temporarilyRevealedColumns.has(columnKey)}
|
||||
onClick={onToggleSensitiveData}
|
||||
>
|
||||
<Eye size={14} strokeWidth={1.5} />
|
||||
<span>
|
||||
{snap.temporarilyRevealedColumns.has(columnKey) ? 'Data revealed (5s)' : 'Show data'}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{snap.editable && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -44,17 +44,30 @@ export const RowContextMenuContent = ({
|
||||
|
||||
const value = row[column.key]
|
||||
const text = formatClipboardValue(value)
|
||||
const isSensitive = snap.sensitiveDataColumns.has(column.key as string)
|
||||
|
||||
void copyToClipboard(text, () => {
|
||||
toast.success('Copied cell value to clipboard')
|
||||
if (isSensitive) {
|
||||
toast.warning('Copied sensitive data to clipboard')
|
||||
} else {
|
||||
toast.success('Copied cell value to clipboard')
|
||||
}
|
||||
})
|
||||
}, [activeCellPosition, row, snap.gridColumns])
|
||||
}, [activeCellPosition, row, snap.gridColumns, snap.sensitiveDataColumns])
|
||||
|
||||
const onCopyRowContent = useCallback(() => {
|
||||
const hasSensitiveColumns = snap.gridColumns.some((col) =>
|
||||
snap.sensitiveDataColumns.has(col.key as string)
|
||||
)
|
||||
|
||||
void copyToClipboard(JSON.stringify(row), () => {
|
||||
toast.success('Copied row to clipboard')
|
||||
if (hasSensitiveColumns) {
|
||||
toast.warning('Copied row containing sensitive data to clipboard')
|
||||
} else {
|
||||
toast.success('Copied row to clipboard')
|
||||
}
|
||||
})
|
||||
}, [row])
|
||||
}, [row, snap.gridColumns, snap.sensitiveDataColumns])
|
||||
|
||||
const getRowAndColumn = useCallback(() => {
|
||||
if (!activeCellPosition) return null
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface SavedState {
|
||||
filters?: string[]
|
||||
sorts?: string[]
|
||||
gridColumns: CalculatedColumn<any, any>[]
|
||||
sensitiveDataColumns?: string[]
|
||||
}
|
||||
|
||||
export interface DragItem {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function isColumnMasked(
|
||||
columnKey: string,
|
||||
sensitiveDataColumns: Set<string>,
|
||||
temporarilyRevealedColumns: Set<string>
|
||||
): boolean {
|
||||
return sensitiveDataColumns.has(columnKey) && !temporarilyRevealedColumns.has(columnKey)
|
||||
}
|
||||
@@ -35,15 +35,15 @@ function useLastSeen({ projectRef, enabled }: { projectRef: string; enabled?: bo
|
||||
} {
|
||||
const now = useRef(new Date()).current
|
||||
|
||||
const query = useLogsQuery(
|
||||
const query = useLogsQuery({
|
||||
enabled,
|
||||
projectRef,
|
||||
{
|
||||
initialParams: {
|
||||
iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
|
||||
iso_timestamp_end: now.toISOString(),
|
||||
sql: "-- last-used-secret-api-keys\nSELECT unix_millis(max(timestamp)) as timestamp, apikey.`hash` FROM edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request cross join unnest(request.sb) as sb cross join unnest(sb.apikey) as sbapikey cross join unnest(sbapikey.apikey) as apikey WHERE apikey.error is null and apikey.`hash` is not null and apikey.prefix like 'sb_secret_%' GROUP BY apikey.`hash`",
|
||||
},
|
||||
enabled
|
||||
)
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
if (query.isLoading || !query.logData) {
|
||||
|
||||
@@ -18,13 +18,11 @@ export function ApiAuthorizationErrorScreen({
|
||||
<div className="flex flex-col gap-3 px-6 pb-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
title="Retry the authorization request from the requesting app."
|
||||
description={
|
||||
<>
|
||||
Retry the authorization request from the requesting app.
|
||||
{error && (
|
||||
<span className="mt-1 block text-foreground-lighter">Error: {error.message}</span>
|
||||
)}
|
||||
</>
|
||||
error && (
|
||||
<span className="mt-1 block text-foreground-lighter">Error: {error.message}</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button variant="default" block asChild>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { type ReactNode } from 'react'
|
||||
import type { UseFormReturn } from 'react-hook-form'
|
||||
import {
|
||||
@@ -194,6 +195,12 @@ function OrganizationsEmptyState(): ReactNode {
|
||||
type="warning"
|
||||
title="No organizations found"
|
||||
description="Create an organization before authorizing this request."
|
||||
actions={[
|
||||
// [Joshen] JFYI this is a short term solution to guide users with creating an org from here
|
||||
<Button asChild key="new-org" variant="default">
|
||||
<Link href="/new">Create an organization</Link>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ export function ApiAuthorizationInvalidScreen({
|
||||
<div className="flex flex-col gap-3 px-6 pb-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
description={`Retry the authorization request from the requesting app. The URL is missing parameter${
|
||||
title="Retry the authorization request from the requesting app. "
|
||||
description={`The URL is missing parameter${
|
||||
isPlural ? 's' : ''
|
||||
}: ${missingParameters.join(', ')}.`}
|
||||
/>
|
||||
|
||||
@@ -127,6 +127,12 @@ export const useIsJitDbAccessEnabled = () => {
|
||||
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
|
||||
}
|
||||
|
||||
export const useIsSqlEditorManualSaveEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave')
|
||||
return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
|
||||
}
|
||||
|
||||
export const useIsRLSTesterEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]
|
||||
|
||||
@@ -35,6 +35,7 @@ import { JitDbAccessPreview } from './JitDbAccessPreview'
|
||||
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
|
||||
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
|
||||
import { RLSTesterPreview } from './RLSTesterPreview'
|
||||
import { SqlEditorManualSavePreview } from './SqlEditorManualSavePreview'
|
||||
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
|
||||
import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews'
|
||||
import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
|
||||
@@ -52,6 +53,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: <JitDbAccessPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: <RLSTesterPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: <SqlEditorManualSavePreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <IntegrationsLayoutPreview />,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const SqlEditorManualSavePreview = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Switch the SQL Editor from autosaving every edit to saving only when you ask it to.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Enabling this preview will:</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>Stop auto-saving snippet edits as you type</li>
|
||||
<li>Add a Save button next to Run in the SQL Editor toolbar</li>
|
||||
<li>Let you save with Cmd+S at any time</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
const platformWebhooksEnabled = useFlag('platformWebhooks')
|
||||
const jitDbAccessEnabled = useFlag('jitDbAccess')
|
||||
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
|
||||
const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave')
|
||||
|
||||
const unifiedLogsDefaultOptIn = useFlag('unifiedLogsDefaultOptIn')
|
||||
|
||||
@@ -109,7 +110,22 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
isDefaultOptIn: false,
|
||||
getRoute: (ref?: string) => `/project/${ref}/integrations`,
|
||||
},
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE,
|
||||
name: 'Disable snippet auto-saving',
|
||||
discussionsUrl: undefined,
|
||||
isNew: true,
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
enabled: sqlEditorManualSaveEnabled,
|
||||
},
|
||||
].sort((a, b) => Number(b.isNew) - Number(a.isNew)),
|
||||
[unifiedLogsDefaultOptIn, platformWebhooksEnabled, jitDbAccessEnabled, isMarketplaceEnabled]
|
||||
[
|
||||
unifiedLogsDefaultOptIn,
|
||||
platformWebhooksEnabled,
|
||||
jitDbAccessEnabled,
|
||||
isMarketplaceEnabled,
|
||||
sqlEditorManualSaveEnabled,
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'sonner'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogBody,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
import { type AuthTemplate } from './EmailTemplates.types'
|
||||
import { getAuthTemplateType } from './EmailTemplates.utils'
|
||||
@@ -32,6 +34,7 @@ export const ResetTemplateDialog = ({
|
||||
}) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const { can: canUpdateConfig } = useAsyncCheckPermissions(
|
||||
PermissionAction.UPDATE,
|
||||
'custom_config_gotrue'
|
||||
@@ -40,25 +43,40 @@ export const ResetTemplateDialog = ({
|
||||
const { id } = template
|
||||
const templateType = getAuthTemplateType(id)
|
||||
|
||||
const { mutate: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation()
|
||||
const { mutateAsync: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation({
|
||||
onSuccess: (config) => {
|
||||
toast.success('Email template reset to default')
|
||||
onResetSuccess(config)
|
||||
},
|
||||
onError: () => {},
|
||||
})
|
||||
|
||||
const resetTemplateToDefault = async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!templateType) throw new Error('Template type is required')
|
||||
|
||||
resetAuthTemplate(
|
||||
{ projectRef, template: templateType },
|
||||
{
|
||||
onSuccess: (config) => {
|
||||
toast.success('Email template reset to default')
|
||||
onResetSuccess(config)
|
||||
},
|
||||
}
|
||||
)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await resetAuthTemplate({ projectRef, template: templateType })
|
||||
} catch (error) {
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An unknown error occurred while resetting the template'
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
setOpen(next)
|
||||
if (!next) setError(null)
|
||||
}}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="default" type="button" disabled={!canUpdateConfig}>
|
||||
Reset template
|
||||
@@ -73,15 +91,21 @@ export const ResetTemplateDialog = ({
|
||||
: 'This will remove your custom subject line and email body content. The default values will be used instead.'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{error && (
|
||||
<AlertDialogBody>
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Unable to reset email template"
|
||||
description={error}
|
||||
/>
|
||||
</AlertDialogBody>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="warning"
|
||||
loading={isResetting}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
resetTemplateToDefault()
|
||||
}}
|
||||
onClick={resetTemplateToDefault}
|
||||
>
|
||||
Reset
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -124,7 +124,10 @@ const renderTemplateEditor = ({
|
||||
})
|
||||
useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig })
|
||||
useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock })
|
||||
useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock, isPending: false })
|
||||
useAuthTemplateResetMutationMock.mockImplementation((options = {}) => ({
|
||||
mutateAsync: (vars: unknown) => resetTemplateMock(vars, options),
|
||||
isPending: false,
|
||||
}))
|
||||
|
||||
return render(<TemplateEditor template={confirmationTemplate} />)
|
||||
}
|
||||
@@ -253,4 +256,22 @@ describe('TemplateEditor reset to default', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Reset template' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('keeps the dialog open and shows an inline error when reset fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
resetTemplateMock.mockImplementation(async () => {
|
||||
throw new Error('Reset endpoint unavailable')
|
||||
})
|
||||
|
||||
renderTemplateEditor({ hasCustomBody: true })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Reset template' }))
|
||||
const dialog = await screen.findByRole('alertdialog')
|
||||
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
|
||||
|
||||
expect(await within(dialog).findByText('Reset endpoint unavailable')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('Unable to reset email template')).toBeInTheDocument()
|
||||
expect(within(dialog).getByRole('button', { name: 'Reset' })).toBeInTheDocument()
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { HttpResponse } from 'msw'
|
||||
import { toast } from 'sonner'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import SiteUrl from './SiteUrl'
|
||||
import type { AuthConfigResponse } from '@/data/auth/auth-config-query'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
import { addAPIMock } from '@/tests/lib/msw'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
|
||||
useAsyncCheckPermissions: () => ({ can: true, isSuccess: true }),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/constants', async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>('@/lib/constants')
|
||||
return {
|
||||
...actual,
|
||||
get IS_PLATFORM() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const CURRENT_SITE_URL = 'https://old.example.com'
|
||||
|
||||
const renderSiteUrl = () => customRender(<SiteUrl />)
|
||||
|
||||
describe('SiteUrl', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/auth/:ref/config',
|
||||
response: { SITE_URL: CURRENT_SITE_URL } as unknown as AuthConfigResponse,
|
||||
})
|
||||
})
|
||||
|
||||
it('trims leading and trailing whitespace from the site URL before submitting', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const requests: Array<{ body: unknown }> = []
|
||||
addAPIMock({
|
||||
method: 'patch',
|
||||
path: '/platform/auth/:ref/config',
|
||||
response: async ({ request }) => {
|
||||
requests.push({ body: await request.json() })
|
||||
return HttpResponse.json<AuthConfigResponse>({
|
||||
SITE_URL: 'https://new.example.com',
|
||||
} as unknown as AuthConfigResponse)
|
||||
},
|
||||
})
|
||||
|
||||
renderSiteUrl()
|
||||
|
||||
const input = await screen.findByDisplayValue(CURRENT_SITE_URL)
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, ' https://new.example.com ')
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Save changes' }))
|
||||
|
||||
await waitFor(() => expect(requests).toHaveLength(1))
|
||||
expect(requests[0].body).toEqual({ SITE_URL: 'https://new.example.com' })
|
||||
expect(toast.success).toHaveBeenCalledWith('Successfully updated site URL')
|
||||
})
|
||||
|
||||
it('shows a validation error and does not submit when the value is only whitespace', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const requests: Array<{ body: unknown }> = []
|
||||
addAPIMock({
|
||||
method: 'patch',
|
||||
path: '/platform/auth/:ref/config',
|
||||
response: async ({ request }) => {
|
||||
requests.push({ body: await request.json() })
|
||||
return HttpResponse.json<AuthConfigResponse>({
|
||||
SITE_URL: '',
|
||||
} as unknown as AuthConfigResponse)
|
||||
},
|
||||
})
|
||||
|
||||
renderSiteUrl()
|
||||
|
||||
const input = await screen.findByDisplayValue(CURRENT_SITE_URL)
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, ' ')
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Save changes' }))
|
||||
|
||||
expect(await screen.findByText('Must have a Site URL')).toBeInTheDocument()
|
||||
await waitFor(() => expect(requests).toHaveLength(0))
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -22,7 +22,7 @@ import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-muta
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
|
||||
const schema = z.object({
|
||||
SITE_URL: z.string().min(1, 'Must have a Site URL'),
|
||||
SITE_URL: z.string().trim().min(1, 'Must have a Site URL'),
|
||||
})
|
||||
|
||||
const SiteUrl = () => {
|
||||
|
||||
@@ -807,6 +807,7 @@ export const UsersV2 = () => {
|
||||
renderRow(id, props) {
|
||||
return (
|
||||
<Row
|
||||
key={id}
|
||||
{...props}
|
||||
onClick={() => {
|
||||
const user = users.find((u) => u.id === id)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { basename } from 'path'
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { Circle, Code, Minus, Plus, Wind } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -7,10 +6,11 @@ import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui'
|
||||
|
||||
import { DiffEditor } from '@/components/ui/DiffEditor'
|
||||
import type { EdgeFunctionBodyData } from '@/data/edge-functions/edge-function-body-query'
|
||||
import type {
|
||||
EdgeFunctionsDiffResult,
|
||||
FileInfo,
|
||||
FileStatus,
|
||||
import {
|
||||
fileKey,
|
||||
type EdgeFunctionsDiffResult,
|
||||
type FileInfo,
|
||||
type FileStatus,
|
||||
} from '@/hooks/branches/useEdgeFunctionsDiff'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
|
||||
@@ -31,9 +31,6 @@ interface FunctionDiffProps {
|
||||
fileInfos: FileInfo[]
|
||||
}
|
||||
|
||||
// Helper to canonicalize file identifiers to prevent mismatch due to differing root paths
|
||||
const fileKey = (fullPath: string) => basename(fullPath)
|
||||
|
||||
// Helper to get the status color for file indicators
|
||||
const getStatusColor = (status: FileStatus): string => {
|
||||
switch (status) {
|
||||
|
||||
@@ -158,14 +158,17 @@ export const ExtensionRow = ({ extension }: ExtensionRowProps) => {
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
checked={isOn}
|
||||
onCheckedChange={() =>
|
||||
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
|
||||
}
|
||||
/>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Switch
|
||||
aria-label="Toggle extension"
|
||||
disabled={disabled}
|
||||
checked={isOn}
|
||||
onCheckedChange={() =>
|
||||
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{disabled && (
|
||||
<TooltipContent side="bottom">
|
||||
|
||||
@@ -29,7 +29,7 @@ export const PublicationsTableItem = ({
|
||||
const isProtected = protectedSchemas.map((x) => x.name).includes(table.schema)
|
||||
|
||||
const [checked, setChecked] = useState(
|
||||
selectedPublication.tables?.find((x: any) => x.id == table.id) != undefined
|
||||
selectedPublication.tables?.find((x) => x.id == table.id) != undefined
|
||||
)
|
||||
|
||||
const { can: canUpdatePublications } = useAsyncCheckPermissions(
|
||||
@@ -48,14 +48,12 @@ export const PublicationsTableItem = ({
|
||||
setChecked(!checked)
|
||||
|
||||
const publicationTables = publication?.tables ?? []
|
||||
const exists = publicationTables.some((x: any) => x.id == table.id)
|
||||
const exists = publicationTables.some((x) => x.id == table.id)
|
||||
const tables = !exists
|
||||
? [`${table.schema}.${table.name}`].concat(
|
||||
publicationTables.map((t: any) => `${t.schema}.${t.name}`)
|
||||
publicationTables.map((t) => `${t.schema}.${t.name}`)
|
||||
)
|
||||
: publicationTables
|
||||
.filter((x: any) => x.id != table.id)
|
||||
.map((x: any) => `${x.schema}.${x.name}`)
|
||||
: publicationTables.filter((x) => x.id != table.id).map((x) => `${x.schema}.${x.name}`)
|
||||
|
||||
updatePublications(
|
||||
{
|
||||
@@ -93,16 +91,19 @@ export const PublicationsTableItem = ({
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Switch
|
||||
size="small"
|
||||
disabled={isToggleDisabled}
|
||||
checked={checked}
|
||||
onClick={() => {
|
||||
if (isWarehousePublication) return
|
||||
toggleReplicationForTable(table, selectedPublication)
|
||||
}}
|
||||
/>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Switch
|
||||
size="small"
|
||||
aria-label={`Toggle replication for ${table.name}`}
|
||||
disabled={isToggleDisabled}
|
||||
checked={checked}
|
||||
onClick={() => {
|
||||
if (isWarehousePublication) return
|
||||
toggleReplicationForTable(table, selectedPublication)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{isProtected && (
|
||||
<TooltipContent side="bottom" className="w-64 text-center">
|
||||
|
||||
@@ -51,6 +51,7 @@ export const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: initialValues,
|
||||
})
|
||||
|
||||
const { mutate: createDatabaseRole, isPending: isCreating } = useDatabaseRoleCreateMutation({
|
||||
|
||||
@@ -207,7 +207,7 @@ export const TableList = ({
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedSchema,
|
||||
schemas: [selectedSchema],
|
||||
},
|
||||
{
|
||||
select(views) {
|
||||
@@ -228,7 +228,7 @@ export const TableList = ({
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedSchema,
|
||||
schemas: [selectedSchema],
|
||||
},
|
||||
{
|
||||
select(materializedViews) {
|
||||
@@ -251,7 +251,7 @@ export const TableList = ({
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedSchema,
|
||||
schemas: [selectedSchema],
|
||||
},
|
||||
{
|
||||
select(foreignTables) {
|
||||
|
||||
+18
-18
@@ -87,16 +87,16 @@ export const EdgeFunctionRecentErrors = ({
|
||||
logData: recentErrorInvocations,
|
||||
isLoading: isLoadingRecentErrorInvocations,
|
||||
error: recentErrorInvocationsError,
|
||||
} = useLogsQuery(
|
||||
projectRef as string,
|
||||
{
|
||||
} = useLogsQuery({
|
||||
projectRef: projectRef!,
|
||||
initialParams: {
|
||||
sql: recentErrorInvocationsSql,
|
||||
iso_timestamp_start: isoTimestampStart,
|
||||
iso_timestamp_end: isoTimestampEnd,
|
||||
},
|
||||
isQueryEnabled,
|
||||
{ useOtel: true }
|
||||
)
|
||||
enabled: isQueryEnabled,
|
||||
options: { useOtel: true },
|
||||
})
|
||||
|
||||
const recentErrorGroupsBase = useMemo(
|
||||
() => getRecentErrorGroupsBase(recentErrorInvocations),
|
||||
@@ -106,16 +106,16 @@ export const EdgeFunctionRecentErrors = ({
|
||||
logData: sinceLastDeployInvocationCountRows,
|
||||
isLoading: isLoadingSinceLastDeployInvocationCount,
|
||||
error: sinceLastDeployInvocationCountError,
|
||||
} = useLogsQuery(
|
||||
projectRef as string,
|
||||
{
|
||||
} = useLogsQuery({
|
||||
projectRef: projectRef!,
|
||||
initialParams: {
|
||||
sql: sinceLastDeployInvocationCountSql,
|
||||
iso_timestamp_start: isoTimestampStart,
|
||||
iso_timestamp_end: isoTimestampEnd,
|
||||
},
|
||||
Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart),
|
||||
{ useOtel: true }
|
||||
)
|
||||
enabled: Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart),
|
||||
options: { useOtel: true },
|
||||
})
|
||||
|
||||
const relatedExecutionIds = useMemo(
|
||||
() => getRelatedExecutionIds(recentErrorGroupsBase),
|
||||
@@ -131,16 +131,16 @@ export const EdgeFunctionRecentErrors = ({
|
||||
logData: functionRuntimeLogs,
|
||||
isLoading: isLoadingFunctionRuntimeLogs,
|
||||
error: functionRuntimeLogsError,
|
||||
} = useLogsQuery(
|
||||
projectRef as string,
|
||||
{
|
||||
} = useLogsQuery({
|
||||
projectRef: projectRef!,
|
||||
initialParams: {
|
||||
sql: functionRuntimeLogsSql,
|
||||
iso_timestamp_start: isoTimestampStart,
|
||||
iso_timestamp_end: isoTimestampEnd,
|
||||
},
|
||||
Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart),
|
||||
{ useOtel: true }
|
||||
)
|
||||
enabled: Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart),
|
||||
options: { useOtel: true },
|
||||
})
|
||||
const queryError =
|
||||
toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export const ProjectCard = ({
|
||||
<>
|
||||
<li className="list-none h-min">
|
||||
<CardButton
|
||||
tabIndex={0}
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
className="h-44 px-0! group pt-5 pb-0 overflow-hidden relative"
|
||||
hideChevron
|
||||
@@ -77,7 +78,7 @@ export const ProjectCard = ({
|
||||
<div className="w-full flex flex-col gap-y-4 justify-between px-5">
|
||||
<div className="flex flex-col gap-y-0.5 relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<h5 className="text-sm shrink truncate pr-5">{name}</h5>
|
||||
<h2 className="text-sm shrink truncate pr-5">{name}</h2>
|
||||
<div onClick={(e) => e.preventDefault()}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -90,6 +91,7 @@ export const ProjectCard = ({
|
||||
e.preventDefault()
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
aria-label={`Project ${name} actions`}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
|
||||
@@ -81,29 +84,34 @@ export const ProjectTableRow = ({
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div>
|
||||
<h5 className="text-sm">{name}</h5>
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={handleCopyProjectRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleCopyProjectRef(e)
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-lighter hover:text-foreground-light px-1 -ml-1"
|
||||
>
|
||||
{projectRef}
|
||||
{isCopied ? (
|
||||
<Check size={12} strokeWidth={1.25} className="text-brand" />
|
||||
) : (
|
||||
<Copy
|
||||
size={12}
|
||||
strokeWidth={1.25}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<h2 className="text-sm">{name}</h2>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={handleCopyProjectRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleCopyProjectRef(e)
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-lighter hover:text-foreground-light px-1 -ml-1"
|
||||
>
|
||||
{projectRef}
|
||||
{isCopied ? (
|
||||
<Check size={12} strokeWidth={1.25} className="text-brand" />
|
||||
) : (
|
||||
<Copy
|
||||
size={12}
|
||||
strokeWidth={1.25}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy project reference</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{(isGithubIntegrated || isVercelIntegrated || hasPartnerIcon) && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
@@ -175,7 +183,7 @@ export const ProjectTableRow = ({
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<MoreVertical />}
|
||||
aria-label="More actions"
|
||||
aria-label={`Project ${name} actions`}
|
||||
className="w-7"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
@@ -38,8 +38,9 @@ import {
|
||||
import { CodeBlock } from 'ui-patterns/CodeBlock'
|
||||
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
|
||||
import { type CronTableColumn } from './CronJobs.constants'
|
||||
import { useDatabaseCronJobRunCommandMutation } from '@/data/database-cron-jobs/database-cron-job-run-mutation'
|
||||
import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
|
||||
import { type CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
|
||||
import { useDatabaseCronJobToggleMutation } from '@/data/database-cron-jobs/database-cron-jobs-toggle-mutation'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
|
||||
@@ -75,8 +76,8 @@ const getNextRun = (schedule: string, lastRun?: string) => {
|
||||
}
|
||||
|
||||
interface CronJobTableCellProps {
|
||||
col: any
|
||||
row: any
|
||||
col: CronTableColumn
|
||||
row: CronJob
|
||||
onSelectEdit: (job: CronJob) => void
|
||||
onSelectDelete: (job: CronJob) => void
|
||||
}
|
||||
@@ -92,19 +93,20 @@ export const CronJobTableCell = ({
|
||||
|
||||
const [showToggleModal, setShowToggleModal] = useState(false)
|
||||
|
||||
const value = row?.[col.id]
|
||||
const value = row?.[col.id as keyof typeof row]
|
||||
const { jobid, schedule, latest_run, status, active, jobname } = row
|
||||
|
||||
const formattedValue =
|
||||
const formattedValue = (
|
||||
col.id === 'jobname' && !jobname
|
||||
? 'No name provided'
|
||||
: col.id === 'lastest_run'
|
||||
? !!value
|
||||
? dayjs(value).valueOf()
|
||||
? dayjs(value as string).valueOf()
|
||||
: undefined
|
||||
: col.id === 'next_run'
|
||||
? getNextRun(schedule, latest_run)
|
||||
: value
|
||||
) as string
|
||||
|
||||
const hasValue = col.id === 'next_run' ? !!formattedValue : col.id in row
|
||||
|
||||
@@ -148,6 +150,7 @@ export const CronJobTableCell = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label="More actions"
|
||||
loading={isRunning}
|
||||
className="h-6 w-6"
|
||||
icon={<MoreVertical />}
|
||||
@@ -203,13 +206,16 @@ export const CronJobTableCell = ({
|
||||
if (col.id === 'active') {
|
||||
return (
|
||||
<Dialog open={showToggleModal} onOpenChange={setShowToggleModal}>
|
||||
<DialogTrigger className="flex items-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
id={`cron-job-active-${jobid}`}
|
||||
size="medium"
|
||||
disabled={isToggling}
|
||||
checked={active}
|
||||
/>
|
||||
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
id={`cron-job-active-${jobid}`}
|
||||
aria-label={`${active ? 'Disable' : 'Enable'} cron job`}
|
||||
size="medium"
|
||||
disabled={isToggling}
|
||||
checked={active}
|
||||
/>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -49,7 +49,7 @@ export type HTTPHeader = { name: string; value: string }
|
||||
|
||||
export type HTTPParameter = { name: string; value: string }
|
||||
|
||||
type CronTableColumn = {
|
||||
export type CronTableColumn = {
|
||||
id: string
|
||||
name: string
|
||||
width?: number
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { ShowApiKey } from '../../Docs/Docs.types'
|
||||
import { GeneralContent } from '@/components/interfaces/Docs/GeneralContent'
|
||||
@@ -16,6 +19,7 @@ interface DocViewProps {
|
||||
}
|
||||
|
||||
export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, page, resource, rpc } = useParams()
|
||||
|
||||
const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef })
|
||||
@@ -31,6 +35,27 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
|
||||
|
||||
const { resources, rpcs } = buildEntityMaps(paths)
|
||||
|
||||
const resourceMissing = !!resource && !(resource in resources)
|
||||
const rpcMissing = !!rpc && !(rpc in rpcs)
|
||||
|
||||
useEffect(() => {
|
||||
const isSchemaReady = !isLoading && !!jsonSchema && !settingsError && !jsonSchemaError
|
||||
if (!isSchemaReady || !projectRef) return
|
||||
if (resourceMissing || rpcMissing) {
|
||||
toast.error(`${resourceMissing ? 'table/view' : 'function'} could not be found`)
|
||||
router.replace(`/project/${projectRef}/integrations/data_api/docs`)
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
jsonSchema,
|
||||
settingsError,
|
||||
jsonSchemaError,
|
||||
projectRef,
|
||||
resourceMissing,
|
||||
rpcMissing,
|
||||
router,
|
||||
])
|
||||
|
||||
if (settingsError || jsonSchemaError) {
|
||||
return <DocViewError error={settingsError || jsonSchemaError} />
|
||||
}
|
||||
|
||||
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user