diff --git a/apps/design-system/registry/default/example/resizable-demo-with-handle.tsx b/apps/design-system/registry/default/example/resizable-demo-with-handle.tsx index fe58cf9aa4b8b..e68f68691094b 100644 --- a/apps/design-system/registry/default/example/resizable-demo-with-handle.tsx +++ b/apps/design-system/registry/default/example/resizable-demo-with-handle.tsx @@ -2,22 +2,22 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' export default function ResizableDemo() { return ( - - + +
One
- - - + + +
Two
- +
Three
diff --git a/apps/design-system/registry/default/example/resizable-demo.tsx b/apps/design-system/registry/default/example/resizable-demo.tsx index 521427569b1f7..9e0ea6354fa0f 100644 --- a/apps/design-system/registry/default/example/resizable-demo.tsx +++ b/apps/design-system/registry/default/example/resizable-demo.tsx @@ -2,22 +2,22 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' export default function ResizableDemo() { return ( - - + +
One
- - - + + +
Two
- +
Three
diff --git a/apps/design-system/registry/default/example/resizable-handle.tsx b/apps/design-system/registry/default/example/resizable-handle.tsx index 29a14b6ca5d62..517e5104ddfc7 100644 --- a/apps/design-system/registry/default/example/resizable-handle.tsx +++ b/apps/design-system/registry/default/example/resizable-handle.tsx @@ -3,16 +3,16 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' export default function ResizableDemo() { return ( - +
Sidebar
- +
Content
diff --git a/apps/design-system/registry/default/example/resizable-vertical.tsx b/apps/design-system/registry/default/example/resizable-vertical.tsx index 6b00df0180e78..367fef3bb893a 100644 --- a/apps/design-system/registry/default/example/resizable-vertical.tsx +++ b/apps/design-system/registry/default/example/resizable-vertical.tsx @@ -2,14 +2,17 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' export default function ResizableDemo() { return ( - - + +
Header
- +
Content
diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index 1bff38834c02a..0181d07022f82 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -335,7 +335,9 @@ If your image has alternate light and dark versions, or you want to make it zoom dark: '/docs/img/supabase-architecture.svg', light: '/docs/img/supabase-architecture--light.svg', }} - zoomable + width={1600} + height={767} + /> ``` @@ -345,7 +347,9 @@ If your image has alternate light and dark versions, or you want to make it zoom dark: '/docs/img/supabase-architecture.svg', light: '/docs/img/supabase-architecture--light.svg', }} - zoomable + width={1600} + height={767} + /> ### Project Variables diff --git a/apps/docs/components/Image.tsx b/apps/docs/components/Image.tsx new file mode 100644 index 0000000000000..bd89a77272bbd --- /dev/null +++ b/apps/docs/components/Image.tsx @@ -0,0 +1,64 @@ +'use client' + +import { useTheme } from 'next-themes' +import NextImage, { ImageProps as NextImageProps } from 'next/image' + +interface StaticImageData { + src: string + height: number + width: number + blurDataURL?: string + blurWidth?: number + blurHeight?: number +} + +interface StaticRequire { + default: StaticImageData +} +type StaticImport = StaticRequire | StaticImageData + +type SourceType = + | string + | { + dark: string | StaticImport + light: string | StaticImport + } + +export interface ImageProps extends Omit { + src: SourceType + caption?: string + containerClassName?: string +} + +/** + * + * This is a shrunk version of the `ui` package Image component. Because of + * Cumulative Layout Shift caused by problems stated in this PR + * https://github.com/supabase/supabase/pull/43026/ that's affecting hash + * navigation, and the need to support captions and light/dark image versions. + * + * Ideally we should solve these issues in that component and re-use it again, + * making sure it doesn't affect other projects consuming the component. + * + */ +const Image = ({ src, alt = '', ...props }: ImageProps) => { + const { resolvedTheme } = useTheme() + const source = + typeof src === 'string' ? src : resolvedTheme?.includes('dark') ? src.dark : src.light + + return ( +
+ + {props.caption &&
{props.caption}
} +
+ ) +} + +export default Image diff --git a/apps/docs/content/guides/ai/choosing-compute-addon.mdx b/apps/docs/content/guides/ai/choosing-compute-addon.mdx index 7c36ccfdd722f..e4f8c1ae905fd 100644 --- a/apps/docs/content/guides/ai/choosing-compute-addon.mdx +++ b/apps/docs/content/guides/ai/choosing-compute-addon.mdx @@ -126,7 +126,9 @@ It is possible to upload more vectors to a single table if Memory allows it (for light: '/docs/img/ai/instance-type/hnsw-dims--light.png', dark: '/docs/img/ai/instance-type/hnsw-dims--dark.png', }} - zoomable + width={1427} + height={862} + /> ## IVFFlat @@ -260,7 +262,9 @@ For 1,000,000 vectors 40 probes results to accuracy of 0.98. Note that exact val light: '/docs/img/ai/going-prod/size-to-rps--light.png', dark: '/docs/img/ai/going-prod/size-to-rps--dark.png', }} - zoomable + width={1427} + height={862} + /> @@ -296,7 +300,9 @@ You can increase the Requests per Second by increasing `m` and `ef_construction` light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png', dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png', }} - zoomable + width={1052} + height={796} + /> @@ -308,7 +314,9 @@ You can increase the Requests per Second by increasing `m` and `ef_construction` light: '/docs/img/ai/instance-type/lists-for-1m--light.png', dark: '/docs/img/ai/instance-type/lists-for-1m--dark.png', }} - zoomable + width={1032} + height={796} + /> @@ -327,7 +335,9 @@ We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikber dark: '/docs/img/ai/instance-type/vecs-benchmark--dark.png', }} className="max-h-[650px]" - zoomable + width={1196} + height={1194} + /> Each test is run for a minimum of 30-40 minutes. They include a series of experiments executed at different concurrency levels to measure the engine's performance under different load types. The results are then averaged. diff --git a/apps/docs/content/guides/ai/engineering-for-scale.mdx b/apps/docs/content/guides/ai/engineering-for-scale.mdx index 8159a4b14659e..55e8990507d8c 100644 --- a/apps/docs/content/guides/ai/engineering-for-scale.mdx +++ b/apps/docs/content/guides/ai/engineering-for-scale.mdx @@ -20,7 +20,9 @@ If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different light: '/docs/img/ai/scaling/engineering-for-scale--single-database--light.png', dark: '/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png', }} - zoomable + width={1600} + height={1145} + /> For example, with 3 collections, called `docs`, `posts`, and `images`, we could expose the "docs" inside the public schema like this: @@ -55,7 +57,9 @@ As you move into production, we recommend splitting your collections into separa light: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png', dark: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png', }} - zoomable + width={1600} + height={1641} + /> You can use as many secondary databases as you need to manage your collections. With this architecture, you have 2 options for accessing collections within your application: @@ -138,5 +142,7 @@ This diagram provides an example architecture that allows you to access the coll light: '/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png', dark: '/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png', }} - zoomable + width={1600} + height={1754} + /> diff --git a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx index 468df1a8d55f3..f0c72e677a785 100644 --- a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx +++ b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx @@ -37,7 +37,9 @@ We'll be saving the Postgres documentation in Postgres, and ChatGPT will be retr light: '/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--light.png', dark: '/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--dark.png', }} - zoomable + + width={1196} + height={1194} /> ### Step 1: Fork the ChatGPT Retrieval Plugin repository diff --git a/apps/docs/content/guides/ai/going-to-prod.mdx b/apps/docs/content/guides/ai/going-to-prod.mdx index 388f714dd0d89..ff7ceb25647aa 100644 --- a/apps/docs/content/guides/ai/going-to-prod.mdx +++ b/apps/docs/content/guides/ai/going-to-prod.mdx @@ -34,7 +34,9 @@ On the other hand, if you need to scale your application, you will need to [crea light: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--light.png', dark: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--dark.png', }} - zoomable + width={1052} + height={796} + /> ## HNSW, understanding `ef_construction`, `ef_search`, and `m` @@ -55,7 +57,9 @@ Search parameters: light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png', dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png', }} - zoomable + width={1052} + height={796} + /> ## IVFFlat, understanding `probes` and `lists` @@ -76,7 +80,9 @@ You can find more examples of how `lists` and `probes` constants affect accuracy light: '/docs/img/ai/going-prod/lists-count--light.png', dark: '/docs/img/ai/going-prod/lists-count--dark.png', }} - zoomable + width={1467} + height={808} + /> ## Performance tips when using indexes @@ -117,5 +123,7 @@ Or take a look at our [pgvector 0.5.0 performance](/blog/increase-performance-pg light: '/docs/img/ai/going-prod/size-to-rps--light.png', dark: '/docs/img/ai/going-prod/size-to-rps--dark.png', }} - zoomable + width={1427} + height={862} + /> diff --git a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx index 0fba3bbb29635..906e2b9c9a2eb 100644 --- a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx +++ b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx @@ -81,6 +81,8 @@ Skip lists are multi-layer linked lists. The bottom layer is a regular linked li light: '/docs/img/ai/vector-indexes/hnsw-indexes/skip-list--light.png', dark: '/docs/img/ai/vector-indexes/hnsw-indexes/skip-list--dark.png', }} + width={1400} + height={700} /> When searching for an element, the algorithm begins at the top layer and traverses its linked list horizontally. If the target element is found, the algorithm stops and returns it. Otherwise if the next element in the list is greater than the target (or `NULL`), the algorithm drops down to the next layer below. Since each layer below is less sparse than the layer above (with the bottom layer connecting all elements), the target will eventually be found. Skip lists offer O(log n) average complexity for both search and insertion/deletion. @@ -93,7 +95,9 @@ A navigable small world (NSW) is a special type of proximity graph that also inc alt="visual of an example navigable small world graph" src="/docs/img/ai/vector-indexes/hnsw-indexes/nsw.png" className="max-h-[600px] mx-auto" - zoomable + width={1016} + height={759} + /> The “navigable” part of NSW specifically refers to the ability to logarithmically scale the greedy search algorithm on the graph, an algorithm that attempts to make only the locally optimal choice at each hop. Without this property, the graph may still be considered a small world with short paths between far-away nodes, but the greedy algorithm tends to miss them. Greedy search is ideal for NSW because it is quick to navigate and has low computational costs. diff --git a/apps/docs/content/guides/auth/architecture.mdx b/apps/docs/content/guides/auth/architecture.mdx index ca2c134da55e9..b4c0a53092b64 100644 --- a/apps/docs/content/guides/auth/architecture.mdx +++ b/apps/docs/content/guides/auth/architecture.mdx @@ -16,6 +16,8 @@ There are four major layers to Supabase Auth: dark: '/docs/img/supabase-architecture.svg', light: '/docs/img/supabase-architecture--light.svg', }} + width={1600} + height={767} /> ## Client layer diff --git a/apps/docs/content/guides/auth/auth-mfa/phone.mdx b/apps/docs/content/guides/auth/auth-mfa/phone.mdx index d1fcc3e86fc2b..c903dd95165e6 100644 --- a/apps/docs/content/guides/auth/auth-mfa/phone.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/phone.mdx @@ -19,6 +19,8 @@ Below is a flow chart illustrating how the Enrollment and Verify APIs work in th dark: '/docs/img/guides/auth-mfa/auth-mfa-phone-flow.svg', }} containerClassName="max-w-[700px]" + width={93} + height={150} /> ### Add enrollment flow diff --git a/apps/docs/content/guides/auth/auth-mfa/totp.mdx b/apps/docs/content/guides/auth/auth-mfa/totp.mdx index cc6eed42b88bc..c645e15f681cf 100644 --- a/apps/docs/content/guides/auth/auth-mfa/totp.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/totp.mdx @@ -19,6 +19,8 @@ Below is a flow chart illustrating how the Enrollment, Challenge, and Verify API dark: '/docs/img/guides/auth-mfa/auth-mfa-flow.svg', }} containerClassName="max-w-[700px]" + width={111} + height={150} /> [TOTP MFA API](/docs/reference/javascript/auth-mfa-api) is free to use and is enabled on all Supabase projects by default. diff --git a/apps/docs/content/guides/auth/signing-keys.mdx b/apps/docs/content/guides/auth/signing-keys.mdx index 672979e4c08c1..2b0ad710e2b67 100644 --- a/apps/docs/content/guides/auth/signing-keys.mdx +++ b/apps/docs/content/guides/auth/signing-keys.mdx @@ -83,6 +83,8 @@ Key rotation and revocation are one of the most important processes for maintain dark: '/docs/img/guides/auth-signing-keys/states.svg', }} containerClassName="max-w-[300px] min-w-[180px]" + width={336} + height={766} />
diff --git a/apps/docs/content/guides/cron.mdx b/apps/docs/content/guides/cron.mdx index ba34687eb4030..ca52b0a431b7e 100644 --- a/apps/docs/content/guides/cron.mdx +++ b/apps/docs/content/guides/cron.mdx @@ -13,6 +13,8 @@ Cron Jobs can be created via SQL or the [Integrations -> Cron](/dashboard/projec dark: '/docs/img/guides/cron/cron.jpg', light: '/docs/img/guides/cron/cron--light.jpg', }} + width={2072} + height={1457} /> Every Job can run SQL snippets or database functions with zero network latency or make an HTTP request, such as invoking a Supabase Edge Function, with ease. diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index ea6ac21320b29..2392ac19cfdcf 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -150,8 +150,10 @@ Serverside-poolers, such as Supabase's [Supavisor](https://github.com/supabase/s light: '/docs/img/guides/database/connecting-to-postgres/how-connection-pooling-works--light.png', }} + width={1851} + height={907} caption="Connecting to the database directly vs using a Connection Pooler" - zoomable + /> They maintain hot connections with the database and intelligently share them with clients only when needed, maximizing the amount of queries a single connection can service. They're best used to manage queries from auto-scaling systems, such as edge and serverless functions. @@ -304,5 +306,7 @@ You can follow the decision flow in the connection method diagram to quickly cho light: '/docs/img/guides/database/connecting-to-postgres/connection-decision-tree-light.svg', }} caption="Choosing between direct Postgres connections and connection pooling" - zoomable + width={291} + height={150} + /> diff --git a/apps/docs/content/guides/database/connection-management.mdx b/apps/docs/content/guides/database/connection-management.mdx index e3055e0337fba..9d8f9433688b7 100644 --- a/apps/docs/content/guides/database/connection-management.mdx +++ b/apps/docs/content/guides/database/connection-management.mdx @@ -27,11 +27,13 @@ These numbers are generalizations and depends on other Supabase products that yo Database client connections chart For Teams and Enterprise plans, Supabase provides Advanced Telemetry charts directly within the Dashboard. The `Database client connections` chart displays historical connection data broken down by connection type: diff --git a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx index f8d36deae55ce..acf82ca2796ff 100644 --- a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx +++ b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx @@ -15,11 +15,13 @@ Wrappers introduce some new terminology and different workflows. Foreign Data Wrappers (FDW) ### Remote servers @@ -114,11 +116,13 @@ select cron.schedule( FDW with pg_cron This process can be taxing on your database if you are moving large amounts of data. Often, it's better to use an external tool for batch ETL, such as [Fivetran](https://fivetran.com/) or [Airbyte](https://airbyte.io/). diff --git a/apps/docs/content/guides/database/orioledb.mdx b/apps/docs/content/guides/database/orioledb.mdx index 08e58ec8eb6f8..ab1e354f83711 100644 --- a/apps/docs/content/guides/database/orioledb.mdx +++ b/apps/docs/content/guides/database/orioledb.mdx @@ -12,7 +12,9 @@ OrioleDB addresses PostgreSQL's scalability limitations by removing bottlenecks alt="TPC-C (warehouses = 500)" src="/docs/img/database/orioledb-tpc-c-500-warehouse.png" className="max-w-[550px] !mx-auto border rounded-md" - zoomable + width={1000} + height={609} + /> @@ -53,7 +55,9 @@ To get started with OrioleDB you need to [create a new Supabase project](/dashbo dark: '/docs/img/database/orioledb-creating-project.png', }} className="max-w-[550px] !mx-auto border rounded-md" - zoomable + + width={1376} + height={2034} /> ### Creating tables diff --git a/apps/docs/content/guides/database/partitions.mdx b/apps/docs/content/guides/database/partitions.mdx index d4ca7cd2b2a17..8b7e73dfd30ac 100644 --- a/apps/docs/content/guides/database/partitions.mdx +++ b/apps/docs/content/guides/database/partitions.mdx @@ -13,6 +13,8 @@ Table partitioning is a technique that allows you to divide a large table into s dark: '/docs/img/database/partitions-dark.png', }} className="max-h-[400px] !mx-auto" + width={1600} + height={1075} /> Each partition contains a subset of the data based on a specified criteria, such as a range of values or a specific condition. Partitioning can significantly improve query performance and simplify data management for large datasets. diff --git a/apps/docs/content/guides/database/pgadmin.mdx b/apps/docs/content/guides/database/pgadmin.mdx index 37527961f5b57..588daf8565eb5 100644 --- a/apps/docs/content/guides/database/pgadmin.mdx +++ b/apps/docs/content/guides/database/pgadmin.mdx @@ -30,6 +30,8 @@ hideToc: true light: '/docs/img/guides/database/connecting-to-postgres/pgadmin/register-server-pgAdmin--light.png', }} + width={1851} + height={1190} /> diff --git a/apps/docs/content/guides/database/postgres/indexes.mdx b/apps/docs/content/guides/database/postgres/indexes.mdx index d348cf5d3dc5a..d14f0b5a14ad1 100644 --- a/apps/docs/content/guides/database/postgres/indexes.mdx +++ b/apps/docs/content/guides/database/postgres/indexes.mdx @@ -71,11 +71,13 @@ Here is a simplified diagram of the index we just created (note that in practice B-Tree index example in Postgres You can see that in any large data set, traversing the index to locate a given value can be done in much less operations (O(log n)) than compared to scanning the table one value at a time from top to bottom (O(n)). diff --git a/apps/docs/content/guides/database/replication/replication-monitoring.mdx b/apps/docs/content/guides/database/replication/replication-monitoring.mdx index 4f6baf0444c51..4b63cd0c95dbe 100644 --- a/apps/docs/content/guides/database/replication/replication-monitoring.mdx +++ b/apps/docs/content/guides/database/replication/replication-monitoring.mdx @@ -25,7 +25,9 @@ To monitor your replication pipelines: Destinations List #### Pipeline states @@ -47,7 +49,9 @@ For detailed information about a specific pipeline, click **View status** on the Pipeline Status View #### Replication lag metrics @@ -77,7 +81,9 @@ Table errors occur during the copy phase and affect individual tables. These err Table Error Details **Viewing table error details:** @@ -103,7 +109,9 @@ When a pipeline error occurs, you'll receive an email notification immediately. Pipeline Error Details **Viewing pipeline error details:** @@ -128,7 +136,12 @@ To see detailed logs for all your replication pipelines: 2. Select **Replication** from the log source filter 3. You'll see all logs from your replication pipelines -Replication Logs +Replication Logs diff --git a/apps/docs/content/guides/database/replication/replication-setup.mdx b/apps/docs/content/guides/database/replication/replication-setup.mdx index 4ca941730017c..372b093bb69e5 100644 --- a/apps/docs/content/guides/database/replication/replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/replication-setup.mdx @@ -106,7 +106,9 @@ Before adding destinations, you need to enable replication for your project: Enable Replication ### Step 3: Add a destination @@ -140,7 +142,9 @@ First, create an analytics bucket to store your replicated data: Create new analytics bucket 3. Fill in the bucket details: @@ -148,7 +152,9 @@ First, create an analytics bucket to store your replicated data: Analytics bucket details - **Name**: A unique name for your bucket (e.g., `analytics_warehouse`) @@ -169,7 +175,9 @@ First, create an analytics bucket to store your replicated data: Add Destination 3. Configure the general settings: @@ -263,7 +271,9 @@ Before configuring BigQuery as a destination, set up the following in Google Clo BigQuery Configuration Settings 3. Configure the general settings: @@ -330,7 +340,9 @@ After creating a destination, the replication pipeline will start and appear in Replication Destinations List For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [Replication Monitoring guide](/docs/guides/database/replication/replication-monitoring). @@ -342,7 +354,9 @@ You can manage your pipeline from the destinations list using the actions menu. Pipeline Actions Available actions: diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index f9105daeeb684..9483a832861ae 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -29,11 +29,13 @@ When creating a table, it's best practice to add columns at the same time. Tables and columns You must define the "data type" of each column when it is created. You can add and remove columns at any time after creating a table. @@ -343,11 +345,13 @@ Tables can be "joined" together using Foreign Keys. Foreign Keys This is where the "Relational" naming comes from, as data typically forms some sort of relationship. @@ -421,11 +425,13 @@ Tables belong to `schemas`. Schemas are a way of organizing your tables, often f Schemas and tables If you don't explicitly pass a schema when creating a table, Postgres will assume that you want to create the table in the `public` schema. diff --git a/apps/docs/content/guides/deployment/branching/github-integration.mdx b/apps/docs/content/guides/deployment/branching/github-integration.mdx index 84622862a5c11..d0af319e83910 100644 --- a/apps/docs/content/guides/deployment/branching/github-integration.mdx +++ b/apps/docs/content/guides/deployment/branching/github-integration.mdx @@ -112,11 +112,13 @@ All other configurations, including API, Auth, and seed files, are ignored by de We highly recommend turning on a 'required check' for the Supabase integration. You can do this from your GitHub repository settings. This prevents PRs from being merged when migration checks fail, and stops invalid migrations from being merged into your production branch. Check the "Require status checks to pass before merging" option. ### Email notifications diff --git a/apps/docs/content/guides/deployment/managing-environments.mdx b/apps/docs/content/guides/deployment/managing-environments.mdx index e81bda864b1e6..a992babfa332c 100644 --- a/apps/docs/content/guides/deployment/managing-environments.mdx +++ b/apps/docs/content/guides/deployment/managing-environments.mdx @@ -15,7 +15,9 @@ This guide shows you how to set up your local Supabase development environment t light: '/docs/img/local-dev-environment--light.svg', dark: '/docs/img/local-dev-environment.svg', }} - zoomable + + width={1600} + height={933} /> ## Set up a local environment @@ -164,6 +166,8 @@ In a production environment, we recommend using a CI/CD pipeline to deploy new m light: '/docs/img/local-dev-environment--light.svg', dark: '/docs/img/local-dev-environment.svg', }} + width={1600} + height={933} /> This example uses two Supabase projects, one for production and one for staging. diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index 7b49295fdb87a..10d91f4a8cea6 100644 --- a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx +++ b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx @@ -12,7 +12,9 @@ Running databases is a shared responsibility between you and Supabase. There are light: '/docs/img/platform/shared-responsibility-model--light.png', dark: '/docs/img/platform/shared-responsibility-model--dark.png', }} - zoomable + + width={1674} + height={1405} /> To summarize, you are always responsible for: diff --git a/apps/docs/content/guides/functions/quickstart-dashboard.mdx b/apps/docs/content/guides/functions/quickstart-dashboard.mdx index 0a2f1264ed3ba..1cea338dd5ef5 100644 --- a/apps/docs/content/guides/functions/quickstart-dashboard.mdx +++ b/apps/docs/content/guides/functions/quickstart-dashboard.mdx @@ -45,7 +45,10 @@ Click the **"Deploy a new function"** button and select **"Via Editor"** to crea light: '/docs/img/guides/functions/dashboard/create-edge-function--light.png', dark: '/docs/img/guides/functions/dashboard/create-edge-function--dark.png', }} - zoomable + + + width={2338} + height={926} /> @@ -68,7 +71,10 @@ The dashboard will load your chosen template in the code editor. Here's what the light: '/docs/img/guides/functions/dashboard/edge-function-template--light.png', dark: '/docs/img/guides/functions/dashboard/edge-function-template--dark.png', }} - zoomable + + + width={2430} + height={1248} /> If needed, you can modify this code directly in the browser editor. The function accepts a JSON payload with a `name` field and returns a greeting message. @@ -109,7 +115,10 @@ Click **"Send Request"** to test your function. light: '/docs/img/guides/functions/dashboard/edge-function-test--light.png', dark: '/docs/img/guides/functions/dashboard/edge-function-test--dark.png', }} - zoomable + + + width={2430} + height={1350} /> In this example, we successfully tested our Hello World function by sending a JSON payload with a name field, and received the expected greeting message back. @@ -202,7 +211,10 @@ Go to your project > **Deploy a new function** > **Via AI Assistant**. light: '/docs/img/guides/functions/dashboard/create-ai-edge-function--light.png', dark: '/docs/img/guides/functions/dashboard/create-ai-edge-function--dark.png', }} - zoomable + + + width={2604} + height={926} /> Describe what you want your function to do in the prompt @@ -213,7 +225,10 @@ Describe what you want your function to do in the prompt light: '/docs/img/guides/functions/dashboard/ai-edge-function--light.png', dark: '/docs/img/guides/functions/dashboard/ai-edge-function--dark.png', }} - zoomable + + + width={2768} + height={1836} /> Click **Deploy** and the Assistant will create and deploy the function for you. diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index f8d2cf6ebd83d..00b6121909446 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -104,6 +104,9 @@ You will also need to set secrets for your production Edge Functions. You can do light: '/docs/img/edge-functions-secrets--light.jpg', dark: '/docs/img/edge-functions-secrets.jpg', }} + + width={3757} + height={1525} /> Note that you can paste multiple secrets at a time. diff --git a/apps/docs/content/guides/getting-started/architecture.mdx b/apps/docs/content/guides/getting-started/architecture.mdx index fce7abe164797..c123e65741f12 100644 --- a/apps/docs/content/guides/getting-started/architecture.mdx +++ b/apps/docs/content/guides/getting-started/architecture.mdx @@ -25,6 +25,9 @@ Each Supabase project consists of several tools: dark: '/docs/img/supabase-architecture.svg', light: '/docs/img/supabase-architecture--light.svg', }} + + width={1600} + height={767} /> ### Postgres (database) diff --git a/apps/docs/content/guides/local-development/cli/getting-started.mdx b/apps/docs/content/guides/local-development/cli/getting-started.mdx index d126dbff63ffc..246c90b65d792 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -173,7 +173,10 @@ The Supabase CLI uses Docker containers to manage the local development stack. F dark: '/docs/img/guides/cli/docker-mac.png', light: '/docs/img/guides/cli/docker-mac-light.png', }} - zoomable + + + width={2880} + height={1800} /> @@ -185,7 +188,10 @@ The Supabase CLI uses Docker containers to manage the local development stack. F dark: '/docs/img/guides/cli/docker-win.png', light: '/docs/img/guides/cli/docker-win-light.png', }} - zoomable + + + width={2560} + height={1520} /> diff --git a/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx b/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx index 0da384c9822dc..369f8ab904cdd 100644 --- a/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx +++ b/apps/docs/content/guides/local-development/restoring-downloaded-backup.mdx @@ -16,9 +16,12 @@ If you want to restore your backup to a hosted Supabase project, follow the [Mig First, download your project's backup file from dashboard and identify its backup image version (following the `PG:` prefix): Project Paused: 90 Days Remaining ## Restoring your backup diff --git a/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx b/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx index 2eea2f468569d..dd4784f380c89 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/account-setup.mdx @@ -13,7 +13,10 @@ An AWS Marketplace subscription is linked to exactly one Supabase organization. dark: '/docs/img/guides/platform/aws-marketplace-onboarding-page-extended--dark.png', light: '/docs/img/guides/platform/aws-marketplace-onboarding-page-extended--light.png', }} - zoomable + + + width={3040} + height={1312} /> ## Implications of linking a Supabase organization to a marketplace subscription diff --git a/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx b/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx index 6db2910d231ec..bd5eb285c636a 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/getting-started.mdx @@ -28,8 +28,11 @@ For more details on completing the setup and what it means to link an organizati Supabase product overview on the AWS Marketplace + + + width={2222} + height={2414} +/> @@ -47,8 +50,11 @@ For more details on completing the setup and what it means to link an organizati Supabase purchase options on the AWS Marketplace + + + width={2334} + height={2418} +/> @@ -57,8 +63,11 @@ For more details on completing the setup and what it means to link an organizati Supabase product subscribe + + + width={2270} + height={632} +/> @@ -67,8 +76,11 @@ For more details on completing the setup and what it means to link an organizati Supabase product subscribe + + + width={1944} + height={1254} +/> @@ -80,8 +92,11 @@ For more details on completing the setup and what it means to link an organizati dark: '/docs/img/guides/platform/aws-marketplace-onboarding-page--dark.png', light: '/docs/img/guides/platform/aws-marketplace-onboarding-page--light.png', }} - zoomable - /> + + + width={3048} + height={1058} +/> diff --git a/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx b/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx index 85c9d5a453373..6f0b3c735b0fa 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/invoices.mdx @@ -10,7 +10,10 @@ You can view your invoices in the [AWS Billing and Cost Management console](http Subscription upgrade modal ## What invoices you get from AWS diff --git a/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx b/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx index 434b89ee77308..dfe011b58f3d3 100644 --- a/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx +++ b/apps/docs/content/guides/platform/aws-marketplace/manage-your-subscription.mdx @@ -21,7 +21,10 @@ You can upgrade your plan at any time. The new plan will be active immediately, light: '/docs/img/guides/platform/aws-marketplace-change-plan.png', dark: '/docs/img/guides/platform/aws-marketplace-change-plan.png', }} - zoomable + + + width={2088} + height={2276} /> ### Downgrade @@ -44,7 +47,10 @@ If the downgrade causes you to exceed the [free projects limit](/docs/guides/pla light: '/docs/img/guides/platform/aws-marketplace-configure-auto-renewal.png', dark: '/docs/img/guides/platform/aws-marketplace-configure-auto-renewal.png', }} - zoomable + + + width={2080} + height={2074} /> #### Downgrade to a paid plan diff --git a/apps/docs/content/guides/platform/billing-on-supabase.mdx b/apps/docs/content/guides/platform/billing-on-supabase.mdx index c8d502e283d8a..529b967584034 100644 --- a/apps/docs/content/guides/platform/billing-on-supabase.mdx +++ b/apps/docs/content/guides/platform/billing-on-supabase.mdx @@ -29,8 +29,11 @@ Different plans cannot be mixed within a single organization. For example, you c dark: '/docs/img/guides/platform/billing-overview.png', }} className="max-w-[600px] inline-block" - zoomable - /> + + + width={1600} + height={1475} +/>
## Costs diff --git a/apps/docs/content/guides/platform/compute-and-disk.mdx b/apps/docs/content/guides/platform/compute-and-disk.mdx index 175bbeec3f863..9463f6127cfd4 100644 --- a/apps/docs/content/guides/platform/compute-and-disk.mdx +++ b/apps/docs/content/guides/platform/compute-and-disk.mdx @@ -43,8 +43,11 @@ Compute sizes can be changed by first selecting your project in the dashboard [h light: '/docs/img/guides/platform/compute-size-selection--light.png', dark: '/docs/img/guides/platform/compute-size-selection--dark.png', }} - zoomable + className="max-w-[500px]" + + width={2122} + height={1302} /> We charge hourly for additional compute based on your usage. Read more about [usage-based billing for compute](/docs/guides/platform/manage-your-usage/compute). diff --git a/apps/docs/content/guides/platform/credits.mdx b/apps/docs/content/guides/platform/credits.mdx index dc667e1b55c75..57d9516f066bc 100644 --- a/apps/docs/content/guides/platform/credits.mdx +++ b/apps/docs/content/guides/platform/credits.mdx @@ -13,7 +13,10 @@ Each organization has a credit balance. Credits are applied to future invoices t light: '/docs/img/guides/platform/credit-balance--light.png', dark: '/docs/img/guides/platform/credit-balance--dark.png', }} - zoomable + + + width={2034} + height={388} /> You can find the credit balance on the [organization's billing page](/dashboard/org/_/billing). @@ -50,8 +53,11 @@ If you are interested in larger (> ) credit packages, [rea light: '/docs/img/guides/platform/credit-top-up--light.png', dark: '/docs/img/guides/platform/credit-top-up--dark.png', }} - zoomable + className="max-w-[500px]" + + width={1014} + height={758} /> ## Credit FAQ diff --git a/apps/docs/content/guides/platform/manage-your-subscription.mdx b/apps/docs/content/guides/platform/manage-your-subscription.mdx index 1724b17f99d2b..8ce0e42d9f8b8 100644 --- a/apps/docs/content/guides/platform/manage-your-subscription.mdx +++ b/apps/docs/content/guides/platform/manage-your-subscription.mdx @@ -23,7 +23,10 @@ Upgrades take effect immediately. During the process, you are informed of the as dark: '/docs/img/guides/platform/upgrade-to-pro-plan-modal--dark.png', }} className="max-w-[577px]" - zoomable + + + width={1536} + height={1116} /> If you still have credits in your account, we will use the credits first before charging your card. @@ -39,7 +42,10 @@ Downgrades take effect immediately. During the process, you are informed of the dark: '/docs/img/guides/platform/downgrade-to-free-plan-modal--dark.png', }} className="max-w-[577px]" - zoomable + + + width={2096} + height={1649} /> #### Credits upon downgrade diff --git a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx index e710ac28e77f7..20fde5a3aaaf5 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx @@ -55,7 +55,10 @@ You can view Branching usage on the [organization's usage page](/dashboard/org/_ light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Usage Summary section, you can see how many hours your Preview branches existed during the selected time period. Hover over "Branching Compute Hours" for a detailed breakdown. @@ -66,7 +69,10 @@ In the Usage Summary section, you can see how many hours your Preview branches e light: '/docs/img/guides/platform/usage-summary-branch-hours--light.png', dark: '/docs/img/guides/platform/usage-summary-branch-hours--dark.png', }} - zoomable + + + width={2258} + height={1360} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/compute.mdx b/apps/docs/content/guides/platform/manage-your-usage/compute.mdx index 71771f47f9976..2ca52ffb44c98 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/compute.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/compute.mdx @@ -134,7 +134,10 @@ You can view Compute usage on the [organization's usage page](/dashboard/org/_/u light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Compute Hours section, you can see how many hours of a specific Compute size your projects have used during the selected time period. Hover over a specific date for a daily breakdown. @@ -145,7 +148,10 @@ In the Compute Hours section, you can see how many hours of a specific Compute s light: '/docs/img/guides/platform/usage-compute--light.png', dark: '/docs/img/guides/platform/usage-compute--dark.png', }} - zoomable + + + width={2050} + height={810} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx index 5ea0b0bf6ba17..95af56393ab86 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/disk-size.mdx @@ -109,7 +109,10 @@ You can view Disk size usage on the [organization's usage page](/dashboard/org/_ light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Disk size section, you can see how much disk size your projects have provisioned. @@ -120,7 +123,10 @@ In the Disk size section, you can see how much disk size your projects have prov light: '/docs/img/guides/platform/usage-disk-size--light.png', dark: '/docs/img/guides/platform/usage-disk-size--dark.png', }} - zoomable + + + width={2010} + height={670} /> ### Disk size distribution diff --git a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx index 6fe31598b991e..940b0ca34bf7e 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx @@ -68,7 +68,10 @@ You can view Edge Function Invocations usage on the [organization's usage page]( light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Edge Function Invocations section, you can see how many invocations your projects have had during the selected time period. @@ -79,7 +82,10 @@ In the Edge Function Invocations section, you can see how many invocations your light: '/docs/img/guides/platform/usage-function-invocations--light.png', dark: '/docs/img/guides/platform/usage-function-invocations--dark.png', }} - zoomable + + + width={2008} + height={734} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index 5a3621237a634..a4e2ac2d3bd7f 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -121,7 +121,10 @@ You can view Egress usage on the [organization's usage page](/dashboard/org/_/us light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. Note that this includes the cached egress. @@ -132,6 +135,9 @@ In the Total Egress section, you can see the usage for the selected time period. light: '/docs/img/guides/platform/unified-egress--light.png', dark: '/docs/img/guides/platform/unified-egress.png', }} + + width={803} + height={460} /> Separately, you can see the cached egress right below: @@ -142,6 +148,9 @@ Separately, you can see the cached egress right below: light: '/docs/img/guides/platform/cached-egress--light.png', dark: '/docs/img/guides/platform/cached-egress.png', }} + + width={1422} + height={586} /> ### Custom report @@ -155,7 +164,10 @@ Separately, you can see the cached egress right below: light: '/docs/img/guides/platform/egress-report--light.png', dark: '/docs/img/guides/platform/egress-report--dark.png', }} - zoomable + + + width={2884} + height={948} /> ## Debug usage @@ -172,7 +184,10 @@ On the Advisors [Query performance view](/dashboard/project/_/database/query-per light: '/docs/img/guides/platform/advisor-most-frequent-queries--light.png', dark: '/docs/img/guides/platform/advisor-most-frequent-queries--dark.png', }} - zoomable + + + width={5006} + height={510} /> ### Most requested API endpoints @@ -185,7 +200,10 @@ In the [Logs Explorer](/dashboard/project/_/logs/explorer) you can access Edge L light: '/docs/img/guides/platform/logs-top-paths--light.png', dark: '/docs/img/guides/platform/logs-top-paths--dark.png', }} - zoomable + + + width={4492} + height={1166} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx index 3f9275a09c15f..bbf4e4da8b908 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx @@ -112,4 +112,7 @@ You can view Log Drain Events usage on the [organization's usage page](/dashboar light: '/docs/img/guides/platform/usage-logdrain-events--light.png', dark: '/docs/img/guides/platform/usage-logdrain-events--dark.png', }} + + width={2092} + height={762} /> diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx index 7b492a6287f6c..2b740c2ff6d5b 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx @@ -124,7 +124,10 @@ You can view Monthly Active SSO Users usage on the [organization's usage page](/ light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Monthly Active SSO Users section, you can see the usage for the selected time period. @@ -135,6 +138,9 @@ In the Monthly Active SSO Users section, you can see the usage for the selected light: '/docs/img/guides/platform/usage-mau-sso--light.png', dark: '/docs/img/guides/platform/usage-mau-sso--dark.png', }} + + width={2034} + height={884} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx index 5b0db25b08ad6..348351e26bbfd 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx @@ -25,8 +25,11 @@ Your billing cycle runs from January 1 to January 31. Although User-1 was signed dark: '/docs/img/guides/platform/third-party-mau-auth0-login-screen.png', }} className="max-h-[190px]" - zoomable - /> + + + width={1098} + height={1970} +/> @@ -48,8 +51,11 @@ Your billing cycle runs from January 1 to January 31. Although User-1 was signed dark: '/docs/img/guides/platform/third-party-mau-auth0-login-screen.png', }} className="max-h-[190px]" - zoomable - /> + + + width={1098} + height={1970} +/> @@ -117,6 +123,9 @@ You can view Monthly Active Third-Party Users usage on the [organization's usage light: '/docs/img/guides/platform/usage-mau-third-party--light.png', dark: '/docs/img/guides/platform/usage-mau-third-party--dark.png', }} + + width={2040} + height={1040} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx index b3701d261aca4..ac1fc876b304c 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx @@ -112,7 +112,10 @@ You can view Monthly Active Users usage on the [organization's usage page](/dash light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Monthly Active Users section, you can see the usage for the selected time period. @@ -123,6 +126,9 @@ In the Monthly Active Users section, you can see the usage for the selected time light: '/docs/img/guides/platform/usage-mau--light.png', dark: '/docs/img/guides/platform/usage-mau--dark.png', }} + + width={2040} + height={878} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx index ae8ca9eb218c9..c57ec32b3ce50 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx @@ -74,7 +74,10 @@ You can view Realtime Messages usage on the [organization's usage page](/dashboa light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Realtime Messages section, you can see the usage for the selected time period. @@ -85,7 +88,10 @@ In the Realtime Messages section, you can see the usage for the selected time pe light: '/docs/img/guides/platform/usage-realtime-messages--light.png', dark: '/docs/img/guides/platform/usage-realtime-messages--dark.png', }} - zoomable + + + width={2036} + height={760} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx index 58cc4b2edee7f..c016f2c9bdb78 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx @@ -79,7 +79,10 @@ You can view Realtime Peak Connections usage on the [organization's usage page]( light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Realtime Peak Connections section, you can see the usage for the selected time period. @@ -90,7 +93,10 @@ In the Realtime Peak Connections section, you can see the usage for the selected light: '/docs/img/guides/platform/usage-realtime-peak-connections--light.png', dark: '/docs/img/guides/platform/usage-realtime-peak-connections--dark.png', }} - zoomable + + + width={2046} + height={752} /> ## Exceeding Quotas diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx index 3b499bc37299f..a97814777229c 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx @@ -106,7 +106,10 @@ You can view Storage Image Transformations usage on the [organization's usage pa light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Storage Image Transformations section, you can see how many origin images were transformed during the selected time period. @@ -117,7 +120,10 @@ In the Storage Image Transformations section, you can see how many origin images light: '/docs/img/guides/platform/usage-image-transformations--light.png', dark: '/docs/img/guides/platform/usage-image-transformations--dark.png', }} - zoomable + + + width={2032} + height={848} /> ## Optimize usage diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx index 637304c434733..c383f292ddb12 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx @@ -60,7 +60,10 @@ You can view Storage size usage on the [organization's usage page](/dashboard/or light: '/docs/img/guides/platform/usage-navbar--light.png', dark: '/docs/img/guides/platform/usage-navbar--dark.png', }} - zoomable + + + width={1546} + height={208} /> In the Storage size section, you can see how much storage your projects have used during the selected time period. @@ -71,7 +74,10 @@ In the Storage size section, you can see how much storage your projects have use light: '/docs/img/guides/platform/usage-storage-size--light.png', dark: '/docs/img/guides/platform/usage-storage-size--dark.png', }} - zoomable + + + width={2028} + height={882} /> ### SQL Editor diff --git a/apps/docs/content/guides/platform/project-transfer.mdx b/apps/docs/content/guides/platform/project-transfer.mdx index 856441436fe51..e2ce46e3e4ef9 100644 --- a/apps/docs/content/guides/platform/project-transfer.mdx +++ b/apps/docs/content/guides/platform/project-transfer.mdx @@ -13,7 +13,10 @@ You can freely transfer projects between different organizations. Head to your [ dark: '/docs/img/guides/platform/project-transfer-overview.png', }} className="max-w-[600px] !mx-auto border rounded-md" - zoomable + + + width={677} + height={224} /> Source organization - the organization the project currently belongs to diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index 93764c7378497..bdce1d5b6eb54 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -14,7 +14,9 @@ Read Replicas are additional databases kept in sync with your Primary database. alt="Map view of all project databases." src="/docs/img/guides/platform/read-replicas/map-view.png?v=1" containerClassName="max-w-[700px] !mx-auto" - zoomable + + width={2000} + height={830} /> ## About Read Replicas @@ -46,7 +48,6 @@ You can only read data from a Read Replica. This is in contrast to a Primary dat Read Replicas decision flowchart @@ -128,7 +129,9 @@ In the SQL editor, you can choose if you want to run the query on a particular R alt="SQL editor view." src="/docs/img/guides/platform/read-replicas/sql-editor.png?v=1" containerClassName="max-w-[700px]" - zoomable + + width={2024} + height={1048} /> ### Logging diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 22567b873ddad..a972449f6b1ea 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -80,8 +80,10 @@ The 90-day window allows Supabase to introduce platform changes that may not be During the 90-day restore window a paused project can be restored to the platform with a single button click from [Studio's dashboard page](/dashboard/projects). Project Paused: 90 Days Remaining @@ -91,17 +93,21 @@ After the 90-day restore window, you can download your project's backup file, an - [Restore a backup locally](/docs/guides/local-development/restoring-downloaded-backup) Project Paused: 90 Days Remaining If you upgrade to a paid plan while your project is paused within the 90-day restore window, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support). Project Paused: 90 Days Remaining ### Disk sizing diff --git a/apps/docs/content/guides/platform/your-monthly-invoice.mdx b/apps/docs/content/guides/platform/your-monthly-invoice.mdx index 2f622d7165c26..db213b260d739 100644 --- a/apps/docs/content/guides/platform/your-monthly-invoice.mdx +++ b/apps/docs/content/guides/platform/your-monthly-invoice.mdx @@ -37,7 +37,10 @@ The following invoice was issued on January 6, 2025 with the previous billing cy light: '/docs/img/guides/platform/example-invoice.png', dark: '/docs/img/guides/platform/example-invoice.png', }} - zoomable + + + width={1690} + height={2192} /> 1. The final amount due diff --git a/apps/docs/content/guides/queues/quickstart.mdx b/apps/docs/content/guides/queues/quickstart.mdx index 123553c82937e..85520f2c12541 100644 --- a/apps/docs/content/guides/queues/quickstart.mdx +++ b/apps/docs/content/guides/queues/quickstart.mdx @@ -43,6 +43,9 @@ To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrati dark: '/docs/img/queues-quickstart-install.png', light: '/docs/img/queues-quickstart-install.png', }} + + width={2064} + height={1720} /> On the [Queues page](/dashboard/project/_/integrations/queues/queues): @@ -71,8 +74,11 @@ Queue names can only be lowercase and hyphens and underscores are permitted. dark: '/docs/img/queues-quickstart-create.png', light: '/docs/img/queues-quickstart-create.png', }} - zoomable + className="max-w-lg !mx-auto" + + width={1456} + height={1420} /> ### What happens when you create a queue? @@ -99,6 +105,9 @@ To get started, navigate to the Queues [Settings page](/dashboard/project/_/inte dark: '/docs/img/queues-quickstart-settings.png', light: '/docs/img/queues-quickstart-settings.png', }} + + width={2140} + height={1642} /> ### Enable RLS on your tables in `pgmq` schema @@ -113,6 +122,9 @@ You’ll want to create RLS policies for any Queues you want your client-side co dark: '/docs/img/queues-quickstart-rls.png', light: '/docs/img/queues-quickstart-rls.png', }} + + width={2130} + height={1508} /> ### Grant permissions to `pgmq_public` database functions @@ -135,6 +147,9 @@ To manage your queue permissions, click on the Queue Settings button. dark: '/docs/img/queues-quickstart-queue-settings.png', light: '/docs/img/queues-quickstart-queue-settings.png', }} + + width={2150} + height={1192} /> Then enable the required roles permissions. @@ -145,6 +160,9 @@ Then enable the required roles permissions. dark: '/docs/img/queues-quickstart-roles.png', light: '/docs/img/queues-quickstart-roles-light.png', }} + + width={1271} + height={1315} /> diff --git a/apps/docs/content/guides/realtime/architecture.mdx b/apps/docs/content/guides/realtime/architecture.mdx index 92881e3ae7b02..875b6639b4fa5 100644 --- a/apps/docs/content/guides/realtime/architecture.mdx +++ b/apps/docs/content/guides/realtime/architecture.mdx @@ -15,6 +15,9 @@ Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Er light: '/docs/img/guides/platform/realtime/architecture--light.png', dark: '/docs/img/guides/platform/realtime/architecture--dark.png', }} + + width={1990} + height={2226} /> ## Elixir & Phoenix diff --git a/apps/docs/content/guides/realtime/reports.mdx b/apps/docs/content/guides/realtime/reports.mdx index 72e72a4b927d9..67996800ceb50 100644 --- a/apps/docs/content/guides/realtime/reports.mdx +++ b/apps/docs/content/guides/realtime/reports.mdx @@ -45,11 +45,14 @@ The report displays the total number of connected Realtime clients, showing how Connected Clients chart ### Actions you can take @@ -72,11 +75,14 @@ The report displays the total number of broadcast events sent by clients, showin Broadcast Events chart ### Actions you can take @@ -98,11 +104,14 @@ The report displays the total number of presence events sent by clients, showing Presence Events chart ### Actions you can take @@ -124,11 +133,14 @@ The report displays the total number of Postgres change events received by clien Postgres Changes Events chart ### Actions you can take @@ -151,11 +163,14 @@ The report displays the rate of channel joins per second, showing how frequently Rate of Channel Joins chart ### Actions you can take @@ -176,11 +191,14 @@ The report displays the median payload size in bytes, showing how message sizes Message Payload Size chart ### Actions you can take @@ -203,12 +221,15 @@ The report displays the median replication lag in milliseconds, showing the dela Broadcast from Database Replication Lag chart ### Actions you can take @@ -232,12 +253,15 @@ The report displays the median RLS execution time in milliseconds, showing how l (Read) Private Channel Subscription RLS Execution Time chart ### Actions you can take @@ -262,12 +286,15 @@ The report displays the median RLS execution time in milliseconds, showing how l (Write) Private Channel Subscription RLS Execution Time chart ### Actions you can take @@ -292,11 +319,14 @@ The report displays the total number of HTTP requests made to the Realtime servi Total Requests chart ### Actions you can take @@ -318,11 +348,14 @@ The report displays the total number of response errors from the Realtime API, s Response Errors chart ### Actions you can take @@ -348,11 +381,14 @@ The report displays the average response time in milliseconds, showing how quick Response Speed chart ### Actions you can take diff --git a/apps/docs/content/guides/realtime/settings.mdx b/apps/docs/content/guides/realtime/settings.mdx index d7b0ea319af11..52a749f5843b8 100644 --- a/apps/docs/content/guides/realtime/settings.mdx +++ b/apps/docs/content/guides/realtime/settings.mdx @@ -18,7 +18,10 @@ All changes made in this screen will disconnect all your connected clients to en light: '/docs/img/guides/platform/realtime/realtime-settings--light.png', dark: '/docs/img/guides/platform/realtime/realtime-settings--dark.png', }} - zoomable + + + width={4600} + height={2600} /> You can set the following settings using the Realtime Settings screen in your Dashboard: diff --git a/apps/docs/content/guides/security/platform-audit-logs.mdx b/apps/docs/content/guides/security/platform-audit-logs.mdx index af8c66cbd707b..fbbc03aca1dd8 100644 --- a/apps/docs/content/guides/security/platform-audit-logs.mdx +++ b/apps/docs/content/guides/security/platform-audit-logs.mdx @@ -23,7 +23,10 @@ Platform Audit Logs can be found under your [organization's audit logs](/dashboa light: '/docs/img/guides/security/platform-audit-logs--light.png', dark: '/docs/img/guides/security/platform-audit-logs--dark.png', }} - zoomable + + + width={2414} + height={958} /> For each audit log, you can see additional details by clicking on the log entry: diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index f9c78a6eb4626..500dafdef2953 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -358,6 +358,9 @@ If the tools and communities already exist, with an MIT, Apache 2, or equivalent dark: "/docs/img/supabase-architecture.svg", light: "/docs/img/supabase-architecture--light.svg", }} + + width={1600} + height={767} /> - **[Studio](https://github.com/supabase/supabase/tree/master/apps/studio)** - A dashboard for managing your self-hosted Supabase project diff --git a/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx b/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx index d39870868ca9b..66996a9ae0578 100644 --- a/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx +++ b/apps/docs/content/guides/storage/analytics/query-with-postgres.mdx @@ -24,6 +24,9 @@ The dashboard provides the easiest setup experience: Query with PostgreSQL button on analytics bucket page 3. Enter the **Postgres schema** where you want to create the foreign tables. @@ -31,6 +34,9 @@ The dashboard provides the easiest setup experience: Select destination PostgreSQL schema 4. Click **Connect**. The wrapper is now configured. diff --git a/apps/docs/content/guides/storage/analytics/replication.mdx b/apps/docs/content/guides/storage/analytics/replication.mdx index 842231cb05a36..9e6f716b90bf3 100644 --- a/apps/docs/content/guides/storage/analytics/replication.mdx +++ b/apps/docs/content/guides/storage/analytics/replication.mdx @@ -32,7 +32,10 @@ First, create a new analytics bucket to store your replicated data: Creating a new analytics bucket ### Step 2: Create a publication @@ -61,7 +64,10 @@ Now set up the pipeline to sync data to your analytics bucket: Replication pipeline configuration ## Monitoring your pipeline diff --git a/apps/docs/content/guides/telemetry/reports.mdx b/apps/docs/content/guides/telemetry/reports.mdx index 10261969b6349..d0be20636e9d2 100644 --- a/apps/docs/content/guides/telemetry/reports.mdx +++ b/apps/docs/content/guides/telemetry/reports.mdx @@ -58,11 +58,14 @@ The following charts provide a more advanced and detailed view of your database Memory usage chart | Component | Description | @@ -92,11 +95,14 @@ Actions you can take: CPU usage chart | Category | Description | @@ -128,11 +134,14 @@ Actions you can take: Disk IOPS chart This chart displays read and write IOPS with a reference line showing your compute size's maximum IOPS capacity. @@ -180,11 +189,14 @@ Actions you can take: Disk Size chart | Component | Description | @@ -214,11 +226,14 @@ Actions you can take: Database connections chart | Connection Type | Description | diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index f29a70caec3ff..7c82b3a5d567c 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -1,6 +1,6 @@ import { ArrowDown, Check, X } from 'lucide-react' import Link from 'next/link' -import { Badge, Button, Image } from 'ui' +import { Badge, Button } from 'ui' import { Admonition, type AdmonitionProps } from 'ui-patterns/admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' import { IconPanel } from 'ui-patterns/IconPanel' @@ -13,6 +13,7 @@ import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig' import { CostWarning } from '~/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings' import ButtonCard from '~/components/ButtonCard' import { Extensions } from '~/components/Extensions' +import Image, { type ImageProps } from '~/components/Image' import { JwtGenerator, JwtGeneratorSimple } from '~/components/JwtGenerator' import { MetricsStackCards } from '~/components/MetricsStackCards' import { NavData } from '~/components/NavData' @@ -60,7 +61,7 @@ const components = { IconCheck: Check, IconPanel, IconX: X, - Image: (props: any) => , + Image: (props: ImageProps) => , JwtGenerator, JwtGeneratorSimple, Link, diff --git a/apps/docs/middleware.ts b/apps/docs/middleware.ts index ea55aafa52efc..f4812f9e6201b 100644 --- a/apps/docs/middleware.ts +++ b/apps/docs/middleware.ts @@ -1,17 +1,23 @@ -import { isbot } from 'isbot' -import { NextResponse, type NextRequest } from 'next/server' - import { clientSdkIds } from '~/content/navigation.references' import { BASE_PATH } from '~/lib/constants' +import { stampFirstReferrerCookie } from 'common/first-referrer-cookie' +import { isbot } from 'isbot' +import { NextResponse, type NextRequest } from 'next/server' const REFERENCE_PATH = `${BASE_PATH ?? ''}/reference` export function middleware(request: NextRequest) { const url = new URL(request.url) + + // Non-reference paths: just handle the first-referrer cookie and pass through if (!url.pathname.startsWith(REFERENCE_PATH)) { - return NextResponse.next() + const response = NextResponse.next() + stampFirstReferrerCookie(request, response) + return response } + // Reference paths: existing rewrite logic with cookie stamping on every response + if (isbot(request.headers.get('user-agent'))) { let [, lib, maybeVersion, ...slug] = url.pathname.replace(REFERENCE_PATH, '').split('/') @@ -24,7 +30,9 @@ export function middleware(request: NextRequest) { if (slug.length > 0) { const rewriteUrl = new URL(url) rewriteUrl.pathname = (BASE_PATH ?? '') + '/api/crawlers' - return NextResponse.rewrite(rewriteUrl) + const response = NextResponse.rewrite(rewriteUrl) + stampFirstReferrerCookie(request, response) + return response } } } @@ -33,28 +41,42 @@ export function middleware(request: NextRequest) { if (lib === 'cli') { const rewritePath = [REFERENCE_PATH, 'cli'].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) + const response = NextResponse.rewrite(new URL(rewritePath, request.url)) + stampFirstReferrerCookie(request, response) + return response } if (lib === 'api') { const rewritePath = [REFERENCE_PATH, 'api'].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) + const response = NextResponse.rewrite(new URL(rewritePath, request.url)) + stampFirstReferrerCookie(request, response) + return response } if (lib?.startsWith('self-hosting-')) { const rewritePath = [REFERENCE_PATH, lib].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) + const response = NextResponse.rewrite(new URL(rewritePath, request.url)) + stampFirstReferrerCookie(request, response) + return response } if (clientSdkIds.includes(lib)) { const version = /v\d+/.test(maybeVersion) ? maybeVersion : null const rewritePath = [REFERENCE_PATH, lib, version].filter(Boolean).join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) + const response = NextResponse.rewrite(new URL(rewritePath, request.url)) + stampFirstReferrerCookie(request, response) + return response } - return NextResponse.next() + const response = NextResponse.next() + stampFirstReferrerCookie(request, response) + return response } export const config = { - matcher: '/reference/:path*', + matcher: [ + // Broadened from `/reference/:path*` to stamp first-referrer cookies on all + // docs pages, not just reference paths. Excludes Next.js internals and static files. + '/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)', + ], } diff --git a/apps/studio/components/grid/components/menu/ColumnMenu.tsx b/apps/studio/components/grid/components/menu/ColumnMenu.tsx index aadeae9e4b75f..e6bc661f68d77 100644 --- a/apps/studio/components/grid/components/menu/ColumnMenu.tsx +++ b/apps/studio/components/grid/components/menu/ColumnMenu.tsx @@ -1,13 +1,13 @@ +import { useTableSort } from 'components/grid/hooks/useTableSort' import type { Sort } from 'components/grid/types' -import { ArrowDown, ArrowUp, ChevronDown, Edit, Lock, Trash, Unlock } from 'lucide-react' +import { ArrowDown, ArrowUp, ChevronDown, Copy, Edit, Lock, Trash, Unlock } from 'lucide-react' import type { CalculatedColumn } from 'react-data-grid' - -import { useTableSort } from 'components/grid/hooks/useTableSort' import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, cn, + copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -93,9 +93,19 @@ export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => { Sort Descending + + { + e.stopPropagation() + copyToClipboard(columnName) + }} + > + + Copy name + {snap.editable && ( <> - { return ( <> - + diff --git a/apps/studio/components/ui/DataTable/FilterSideBar.tsx b/apps/studio/components/ui/DataTable/FilterSideBar.tsx index 0811399f4643d..1797cb30cca54 100644 --- a/apps/studio/components/ui/DataTable/FilterSideBar.tsx +++ b/apps/studio/components/ui/DataTable/FilterSideBar.tsx @@ -1,8 +1,8 @@ import { useFlag, useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' -import React from 'react' -import { Button, cn, ResizablePanel } from 'ui' +import React, { useEffect } from 'react' +import { Button, cn, ResizablePanel, usePanelRef } from 'ui' import { FeaturePreviewSidebarPanel } from '../FeaturePreviewSidebarPanel' import { DateRangeDisabled } from './DataTable.types' @@ -13,10 +13,16 @@ import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePrevie import { LOG_DRAIN_TYPES } from '@/components/interfaces/LogDrains/LogDrains.constants' interface FilterSideBarProps { + isFilterBarOpen: boolean + setIsFilterBarOpen: React.Dispatch> dateRangeDisabled?: DateRangeDisabled } -export function FilterSideBar({ dateRangeDisabled }: FilterSideBarProps) { +export function FilterSideBar({ + isFilterBarOpen, + setIsFilterBarOpen, + dateRangeDisabled, +}: FilterSideBarProps) { const router = useRouter() const { ref } = useParams() const { table } = useDataTable() @@ -29,18 +35,31 @@ export function FilterSideBar({ dateRangeDisabled }: FilterSideBarProps) { router.push(`/project/${ref}/logs/explorer`) } + const panelRef = usePanelRef() + + useEffect(() => { + if (isFilterBarOpen) { + panelRef.current?.expand() + } else { + panelRef.current?.collapse() + } + }, [isFilterBarOpen, panelRef]) + return ( { + if (size.inPixels === 0) { + setIsFilterBarOpen(false) + } else if (!isFilterBarOpen) { + setIsFilterBarOpen(true) + } + }} + className={cn('flex flex-col w-full')} >
diff --git a/apps/studio/components/ui/DataTable/providers/ControlsProvider.tsx b/apps/studio/components/ui/DataTable/providers/ControlsProvider.tsx deleted file mode 100644 index ccdf799e8f125..0000000000000 --- a/apps/studio/components/ui/DataTable/providers/ControlsProvider.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useLocalStorage } from 'hooks/misc/useLocalStorage' -import { createContext, Dispatch, SetStateAction, useContext } from 'react' - -interface ControlsContextType { - open: boolean - setOpen: Dispatch> -} - -export const ControlsContext = createContext(null) - -export function ControlsProvider({ children }: { children: React.ReactNode }) { - const [open, setOpen] = useLocalStorage('data-table-controls', true) - - return ( - -
- {children} -
-
- ) -} - -export function useControls() { - const context = useContext(ControlsContext) - - if (!context) { - throw new Error('useControls must be used within a ControlsProvider') - } - - return context as ControlsContextType -} diff --git a/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx b/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx index 2515c748a59dd..7a49998c1a89a 100644 --- a/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx +++ b/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx @@ -7,11 +7,10 @@ import type { Table, VisibilityState, } from '@tanstack/react-table' +import { QuerySearchParamsType } from 'components/interfaces/UnifiedLogs/UnifiedLogs.types' import { createContext, ReactNode, useContext, useMemo } from 'react' -import { QuerySearchParamsType } from 'components/interfaces/UnifiedLogs/UnifiedLogs.types' import { DataTableFilterField } from '../DataTable.types' -import { ControlsProvider } from './ControlsProvider' // REMINDER: read about how to move controlled state out of the useReactTable hook // https://github.com/TanStack/table/discussions/4005#discussioncomment-7303569 @@ -66,11 +65,7 @@ export function DataTableProvider({ [props] ) - return ( - - {children} - - ) + return {children} } export function useDataTable() { diff --git a/apps/studio/package.json b/apps/studio/package.json index e95f81597a0db..f12464045b25c 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -82,9 +82,9 @@ "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", "crypto-js": "^4.2.0", - "dev-tools": "workspace:*", "d3-geo": "^3.1.1", "dayjs": "^1.11.10", + "dev-tools": "workspace:*", "dnd-core": "^16.0.1", "file-saver": "^2.0.5", "framer-motion": "^11.11.17", @@ -190,6 +190,7 @@ "date-fns": "^2.30.0", "eslint-config-supabase": "workspace:*", "eslint-plugin-barrel-files": "^2.0.7", + "eslint-plugin-jsx-a11y": "^6.10.2", "graphql-ws": "5.14.1", "import-in-the-middle": "^2.0.0", "jsdom-testing-mocks": "^1.13.1", diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx index 93ca9663537bb..f0d6a7c638791 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -378,10 +378,10 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
- + { /> - + request.nextUrl.pathname.endsWith(url)) - ) { - return Response.json( - { success: false, message: 'Endpoint not supported on hosted' }, - { status: 404 } - ) + // API route filtering for hosted platform + if (request.nextUrl.pathname.startsWith('/api/')) { + if ( + IS_PLATFORM && + !HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url)) + ) { + return Response.json( + { success: false, message: 'Endpoint not supported on hosted' }, + { status: 404 } + ) + } } + + const response = NextResponse.next() + stampFirstReferrerCookie(request, response) + return response +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|__nextjs).*)'], } diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index d4533c5462e8e..aed3c6826e83e 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -1,6 +1,5 @@ -import { proxy, snapshot, useSnapshot } from 'valtio' - import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common' +import { proxy, snapshot, useSnapshot } from 'valtio' const getInitialState = () => { return { diff --git a/apps/studio/tests/setup/polyfills.ts b/apps/studio/tests/setup/polyfills.ts index e37e5482f5aa5..ba913603e4cbc 100644 --- a/apps/studio/tests/setup/polyfills.ts +++ b/apps/studio/tests/setup/polyfills.ts @@ -1,8 +1,8 @@ -import { TextDecoder, TextEncoder } from 'node:util' import { ReadableStream, TransformStream } from 'node:stream/web' -import { vi } from 'vitest' -import { configMocks } from 'jsdom-testing-mocks' +import { TextDecoder, TextEncoder } from 'node:util' import { act } from '@testing-library/react' +import { configMocks } from 'jsdom-testing-mocks' +import { vi } from 'vitest' configMocks({ act }) diff --git a/apps/www/middleware.ts b/apps/www/middleware.ts new file mode 100644 index 0000000000000..72a74634d8afd --- /dev/null +++ b/apps/www/middleware.ts @@ -0,0 +1,15 @@ +import { stampFirstReferrerCookie } from 'common/first-referrer-cookie' +import { NextResponse, type NextRequest } from 'next/server' + +export function middleware(request: NextRequest) { + const response = NextResponse.next() + stampFirstReferrerCookie(request, response) + return response +} + +export const config = { + matcher: [ + // Match all paths except Next.js internals and static files + '/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)', + ], +} diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 6cc06f681ba3b..46b07c8c93746 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -605,6 +605,33 @@ testRunner('table editor', () => { await deleteTable(page, ref, tableName) }) + test('column actions works as expected', async ({ page, ref }) => { + const tableName = 'pw_table_column_menu' + const colName = 'pw_column' + + // Ensure we're on editor + if (!page.url().includes('/editor')) { + await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) + await waitForTableToLoad(page, ref) + } + + // Create a small table and three rows + await createTable(page, ref, tableName) + await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click() + await page.waitForURL(/\/editor\/\d+\?schema=public$/) + + // Copy the column name + await page.getByRole('columnheader', { name: colName }).getByRole('button').nth(1).click() + await page.getByRole('menuitem', { name: 'Copy name' }).click() + + await page.waitForTimeout(500) + const copiedTableResult = await page.evaluate(() => navigator.clipboard.readText()) + expect(copiedTableResult).toBe(colName) + + // Cleanup + await deleteTable(page, ref, tableName) + }) + test('importing, pagination and large data actions works as expected', async ({ page, ref }) => { await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) const tableNameDataActions = 'pw_table_data' diff --git a/packages/common/first-referrer-cookie.test.ts b/packages/common/first-referrer-cookie.test.ts new file mode 100644 index 0000000000000..ce308e2d24487 --- /dev/null +++ b/packages/common/first-referrer-cookie.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, it } from 'vitest' + +import { + buildFirstReferrerData, + FIRST_REFERRER_COOKIE_NAME, + hasPaidSignals, + isExternalReferrer, + parseFirstReferrerCookie, + serializeFirstReferrerCookie, + shouldRefreshCookie, +} from './first-referrer-cookie' + +describe('first-referrer-cookie', () => { + describe('isExternalReferrer', () => { + it('returns false for supabase domains', () => { + expect(isExternalReferrer('https://supabase.com')).toBe(false) + expect(isExternalReferrer('https://www.supabase.com')).toBe(false) + expect(isExternalReferrer('https://docs.supabase.com')).toBe(false) + }) + + it('returns true for external domains', () => { + expect(isExternalReferrer('https://google.com')).toBe(true) + expect(isExternalReferrer('https://chatgpt.com')).toBe(true) + }) + + it('returns true for http:// referrers', () => { + expect(isExternalReferrer('http://google.com')).toBe(true) + expect(isExternalReferrer('http://example.org/page')).toBe(true) + }) + + it('returns false for invalid values', () => { + expect(isExternalReferrer('')).toBe(false) + expect(isExternalReferrer('not-a-url')).toBe(false) + }) + }) + + describe('buildFirstReferrerData', () => { + it('handles malformed landing URL gracefully', () => { + const data = buildFirstReferrerData({ + referrer: 'https://google.com', + landingUrl: 'not-a-valid-url', + }) + + expect(data.referrer).toBe('https://google.com') + expect(data.landing_url).toBe('not-a-valid-url') + expect(data.utms).toEqual({}) + expect(data.click_ids).toEqual({}) + }) + + it('extracts utm and click-id params from landing url', () => { + const data = buildFirstReferrerData({ + referrer: 'https://www.google.com/', + landingUrl: + 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456', + }) + + expect(data.referrer).toBe('https://www.google.com/') + expect(data.landing_url).toBe( + 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456' + ) + + expect(data.utms).toEqual({ + utm_source: 'google', + utm_medium: 'cpc', + utm_campaign: 'test', + }) + + expect(data.click_ids).toEqual({ + gclid: 'abc123', + msclkid: 'xyz456', + }) + }) + }) + + describe('serialize / parse', () => { + it('round-trips valid cookie payloads', () => { + const input = buildFirstReferrerData({ + referrer: 'https://www.google.com/', + landingUrl: 'https://supabase.com/pricing?utm_source=google', + }) + + const encoded = serializeFirstReferrerCookie(input) + const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`) + + expect(parsed).toEqual(input) + }) + + it('returns null for empty string', () => { + expect(parseFirstReferrerCookie('')).toBeNull() + }) + + it('parses cookie from header with multiple cookies', () => { + const input = buildFirstReferrerData({ + referrer: 'https://google.com/', + landingUrl: 'https://supabase.com/', + }) + const encoded = serializeFirstReferrerCookie(input) + const header = `session=abc123; ${FIRST_REFERRER_COOKIE_NAME}=${encoded}; theme=dark` + + expect(parseFirstReferrerCookie(header)).toEqual(input) + }) + + it('returns null for malformed json', () => { + expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=%7Bnot-json`)).toBeNull() + }) + + it('returns null for invalid payload shape', () => { + const encoded = encodeURIComponent(JSON.stringify({ foo: 'bar' })) + expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)).toBeNull() + }) + + it('drops non-string values in utms/click_ids', () => { + const encoded = encodeURIComponent( + JSON.stringify({ + referrer: 'https://www.google.com/', + landing_url: 'https://supabase.com/pricing', + utms: { utm_source: 'google', utm_medium: 123 }, + click_ids: { gclid: 'abc', msclkid: null }, + ts: 123, + }) + ) + + const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`) + + expect(parsed).toEqual({ + referrer: 'https://www.google.com/', + landing_url: 'https://supabase.com/pricing', + utms: { utm_source: 'google' }, + click_ids: { gclid: 'abc' }, + ts: 123, + }) + }) + }) + + describe('hasPaidSignals', () => { + it('detects click IDs', () => { + expect(hasPaidSignals(new URL('https://supabase.com/?gclid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?fbclid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?msclkid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?gbraid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?wbraid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?rdt_cid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?ttclid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?twclid=abc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?li_fat_id=abc'))).toBe(true) + }) + + it('detects paid utm_medium values', () => { + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=cpc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=ppc'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_search'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paidsocial'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_social'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=display'))).toBe(true) + }) + + it('is case-insensitive for utm_medium', () => { + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=CPC'))).toBe(true) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=Paid_Search'))).toBe(true) + }) + + it('returns false for organic traffic', () => { + expect(hasPaidSignals(new URL('https://supabase.com/'))).toBe(false) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_source=google'))).toBe(false) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=email'))).toBe(false) + expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=organic'))).toBe(false) + }) + }) + + describe('shouldRefreshCookie', () => { + it('stamps when no cookie and external referrer', () => { + expect( + shouldRefreshCookie(false, { + referrer: 'https://google.com', + url: 'https://supabase.com/', + }) + ).toEqual({ stamp: true }) + }) + + it('skips when no cookie and internal referrer', () => { + expect( + shouldRefreshCookie(false, { + referrer: 'https://supabase.com/docs', + url: 'https://supabase.com/dashboard', + }) + ).toEqual({ stamp: false }) + }) + + it('skips when cookie exists and no paid signals', () => { + expect( + shouldRefreshCookie(true, { + referrer: 'https://google.com', + url: 'https://supabase.com/', + }) + ).toEqual({ stamp: false }) + }) + + it('refreshes when cookie exists but URL has paid signals', () => { + expect( + shouldRefreshCookie(true, { + referrer: 'https://google.com', + url: 'https://supabase.com/?gclid=abc123', + }) + ).toEqual({ stamp: true }) + + expect( + shouldRefreshCookie(true, { + referrer: 'https://google.com', + url: 'https://supabase.com/?utm_medium=cpc&utm_source=google', + }) + ).toEqual({ stamp: true }) + }) + + it('skips when no cookie and no referrer (direct navigation)', () => { + expect(shouldRefreshCookie(false, { referrer: '', url: 'https://supabase.com/' })).toEqual({ + stamp: false, + }) + }) + + it('handles malformed URL gracefully', () => { + expect( + shouldRefreshCookie(true, { + referrer: 'https://google.com', + url: 'not-a-valid-url', + }) + ).toEqual({ stamp: false }) + }) + }) +}) diff --git a/packages/common/first-referrer-cookie.ts b/packages/common/first-referrer-cookie.ts new file mode 100644 index 0000000000000..cfa7d6b7131f8 --- /dev/null +++ b/packages/common/first-referrer-cookie.ts @@ -0,0 +1,307 @@ +/** + * Shared utilities for the cross-app first-referrer handoff cookie. + * + * The `_sb_first_referrer` cookie is written by edge middleware on `apps/www`, + * `apps/docs`, and `apps/studio` when a user arrives from an + * external source. Studio reads it on the first telemetry pageview to recover + * external attribution context that would otherwise be lost at the app boundary. + * + * The cookie is normally write-once (365-day TTL, domain=supabase.com), but is + * refreshed when a returning visitor arrives with paid traffic signals (click IDs + * or paid UTM medium values) to ensure paid attribution overrides stale organic data. + */ + +// --------------------------------------------------------------------------- +// Structural types for Next.js middleware request/response +// --------------------------------------------------------------------------- +// Using structural interfaces instead of importing NextRequest/NextResponse +// avoids version conflicts when different apps pin different Next.js versions +// (e.g. studio on Next 15, docs/www on Next 16). + +interface MiddlewareRequest { + headers: { get(name: string): string | null } + cookies: { has(name: string): boolean } + url: string + nextUrl: { hostname: string } +} + +interface MiddlewareResponse { + cookies: { + set( + name: string, + value: string, + options?: { + path?: string + sameSite?: 'lax' | 'strict' | 'none' + secure?: boolean + domain?: string + maxAge?: number + } + ): void + } +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +export const FIRST_REFERRER_COOKIE_NAME = '_sb_first_referrer' + +/** 365 days in seconds */ +export const FIRST_REFERRER_COOKIE_MAX_AGE = 365 * 24 * 60 * 60 + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface FirstReferrerData { + /** The external referrer URL (e.g. https://www.google.com/) */ + referrer: string + /** The landing URL on our site when the external referrer was captured */ + landing_url: string + /** UTM params parsed from the landing URL (e.g. utm_source, utm_medium) */ + utms: Record + /** Ad-network click IDs parsed from the landing URL */ + click_ids: Record + /** Unix timestamp (ms) when the cookie was written */ + ts: number +} + +// --------------------------------------------------------------------------- +// Referrer classification +// --------------------------------------------------------------------------- + +/** + * Returns true if the referrer URL points to an external (non-Supabase) domain. + * Handles malformed URLs gracefully by returning false. + */ +export function isExternalReferrer(referrer: string): boolean { + if (!referrer) return false + try { + const hostname = new URL(referrer).hostname + return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com') + } catch { + return false + } +} + +// --------------------------------------------------------------------------- +// UTM + click-ID extraction +// --------------------------------------------------------------------------- + +const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const + +const CLICK_ID_KEYS = [ + 'gclid', // Google Ads + 'gbraid', // Google Ads (iOS) + 'wbraid', // Google Ads (iOS) + 'msclkid', // Microsoft Ads (Bing) + 'fbclid', // Meta (Facebook/Instagram) + 'rdt_cid', // Reddit Ads + 'ttclid', // TikTok Ads + 'twclid', // X Ads (Twitter) + 'li_fat_id', // LinkedIn Ads +] as const + +function pickParams( + searchParams: URLSearchParams, + keys: readonly string[] +): Record { + const result: Record = {} + for (const key of keys) { + const value = searchParams.get(key) + if (value) { + result[key] = value + } + } + return result +} + +function toStringRecord(value: unknown): Record { + if (!value || typeof value !== 'object') return {} + + return Object.fromEntries( + Object.entries(value as Record).filter( + ([key, v]) => typeof key === 'string' && typeof v === 'string' + ) + ) as Record +} + +// --------------------------------------------------------------------------- +// Build cookie payload from a request (edge-compatible) +// --------------------------------------------------------------------------- + +/** + * Build a `FirstReferrerData` payload from raw request values. + * Intended for use in Next.js middleware where `document` is not available. + */ +export function buildFirstReferrerData({ + referrer, + landingUrl, +}: { + referrer: string + landingUrl: string +}): FirstReferrerData { + let utms: Record = {} + let click_ids: Record = {} + + try { + const url = new URL(landingUrl) + utms = pickParams(url.searchParams, UTM_KEYS) + click_ids = pickParams(url.searchParams, CLICK_ID_KEYS) + } catch { + // If landing URL is malformed, just skip param extraction + } + + return { + referrer, + landing_url: landingUrl, + utms, + click_ids, + ts: Date.now(), + } +} + +// --------------------------------------------------------------------------- +// Serialize / parse +// --------------------------------------------------------------------------- + +export function serializeFirstReferrerCookie(data: FirstReferrerData): string { + return encodeURIComponent(JSON.stringify(data)) +} + +// --------------------------------------------------------------------------- +// Paid-signal detection +// --------------------------------------------------------------------------- + +const PAID_UTM_MEDIUMS = new Set([ + 'cpc', + 'ppc', + 'paid_search', + 'paidsocial', + 'paid_social', + 'display', +]) + +/** + * Returns true if the URL contains ad-network click IDs or paid UTM medium values. + * These indicate the user arrived via a paid campaign, which should override + * stale organic attribution. + */ +export function hasPaidSignals(url: URL): boolean { + for (const key of CLICK_ID_KEYS) { + if (url.searchParams.has(key)) return true + } + const medium = url.searchParams.get('utm_medium')?.toLowerCase() + return medium !== undefined && PAID_UTM_MEDIUMS.has(medium) +} + +/** + * Decides whether the first-referrer cookie should be (re-)stamped. + * + * - No cookie + external referrer → stamp (first visit attribution) + * - Cookie exists + paid signals in URL → stamp (paid traffic refresh) + * - Otherwise → skip + */ +export function shouldRefreshCookie( + existingCookie: boolean, + request: { referrer: string; url: string } +): { stamp: boolean } { + if (!existingCookie) { + return { stamp: isExternalReferrer(request.referrer) } + } + + try { + const url = new URL(request.url) + return { stamp: hasPaidSignals(url) } + } catch { + return { stamp: false } + } +} + +// --------------------------------------------------------------------------- +// Middleware helper — shared across apps/www, apps/docs, and apps/studio +// --------------------------------------------------------------------------- + +/** + * Stamp the first-referrer cookie on a Next.js middleware response if the + * request warrants it. This is the single entry point for all app middleware + * files — call it with the incoming request and outgoing response. + * + * On *.supabase.com the cookie is set with `domain=supabase.com` so it's + * readable across all subdomains (www, docs, studio). On other hosts + * (localhost, preview deploys) the domain is left unset so the browser + * stores a host-only cookie instead of rejecting an invalid domain. + */ +export function stampFirstReferrerCookie(request: MiddlewareRequest, response: MiddlewareResponse): void { + const referrer = request.headers.get('referer') ?? '' + + const { stamp } = shouldRefreshCookie(request.cookies.has(FIRST_REFERRER_COOKIE_NAME), { + referrer, + url: request.url, + }) + + if (!stamp) return + + const data = buildFirstReferrerData({ + referrer, + landingUrl: request.url, + }) + + response.cookies.set(FIRST_REFERRER_COOKIE_NAME, serializeFirstReferrerCookie(data), { + path: '/', + sameSite: 'lax', + ...(request.nextUrl.hostname === 'supabase.com' || + request.nextUrl.hostname.endsWith('.supabase.com') + ? { domain: 'supabase.com', secure: true } + : {}), + maxAge: FIRST_REFERRER_COOKIE_MAX_AGE, + }) +} + +// --------------------------------------------------------------------------- +// Parse cookie from document.cookie header (client-side) +// --------------------------------------------------------------------------- + +export function parseFirstReferrerCookie(cookieHeader: string): FirstReferrerData | null { + try { + const cookies = cookieHeader.split(';') + const match = cookies + .map((c) => c.trim()) + .find((c) => c.startsWith(`${FIRST_REFERRER_COOKIE_NAME}=`)) + + if (!match) return null + + const value = match.slice(`${FIRST_REFERRER_COOKIE_NAME}=`.length) + const parsed = JSON.parse(decodeURIComponent(value)) as unknown + + if (!parsed || typeof parsed !== 'object') return null + + const parsedRecord = parsed as Record + const referrer = parsedRecord.referrer + const landingUrl = parsedRecord.landing_url + + if (typeof referrer !== 'string' || typeof landingUrl !== 'string') { + return null + } + + const utmsRaw = parsedRecord.utms + const clickIdsRaw = parsedRecord.click_ids + const tsRaw = parsedRecord.ts + + const utms = toStringRecord(utmsRaw) + const click_ids = toStringRecord(clickIdsRaw) + + const ts = typeof tsRaw === 'number' && Number.isFinite(tsRaw) ? tsRaw : Date.now() + + return { + referrer, + landing_url: landingUrl, + utms, + click_ids, + ts, + } + } catch { + return null + } +} diff --git a/packages/common/index.tsx b/packages/common/index.tsx index 87b60f4e6f996..ee8ebe133070c 100644 --- a/packages/common/index.tsx +++ b/packages/common/index.tsx @@ -10,5 +10,6 @@ export * from './helpers' export * from './hooks' export * from './MetaFavicons/pages-router' export * from './Providers' +export * from './first-referrer-cookie' export * from './telemetry' export * from './telemetry-utils' diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 29c416c8bd90d..36b3ea4ee011c 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -12,6 +12,8 @@ import { hasConsented } from './consent-state' import { IS_PLATFORM, IS_PROD, LOCAL_STORAGE_KEYS } from './constants' import { useFeatureFlags } from './feature-flags' import { post } from './fetchWrappers' +import type { FirstReferrerData } from './first-referrer-cookie' +import { isExternalReferrer, parseFirstReferrerCookie } from './first-referrer-cookie' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useParams, useTelemetryCookie } from './hooks' import { posthogClient, type ClientTelemetryEvent } from './posthog-client' @@ -96,25 +98,25 @@ function getFirstTouchAttributionProps(telemetryData: SharedTelemetryData) { } } -function isExternalReferrer(referrer: string) { - try { - const hostname = new URL(referrer).hostname - return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com') - } catch { - return false - } +interface HandlePageTelemetryOptions { + apiUrl: string + pathname?: string + featureFlags?: Record + slug?: string + ref?: string + telemetryDataOverride?: SharedTelemetryData + firstReferrerData?: FirstReferrerData | null } -function handlePageTelemetry( - API_URL: string, - pathname?: string, - featureFlags?: { - [key: string]: unknown - }, - slug?: string, - ref?: string, - telemetryDataOverride?: SharedTelemetryData -) { +function handlePageTelemetry({ + apiUrl: API_URL, + pathname, + featureFlags, + slug, + ref, + telemetryDataOverride, + firstReferrerData, +}: HandlePageTelemetryOptions) { // Send to PostHog client-side (only in browser) if (typeof window !== 'undefined') { const livePageData = getSharedTelemetryData(pathname) @@ -133,10 +135,51 @@ function handlePageTelemetry( referrer: shouldUseCookieReferrer ? cookieReferrer! : liveReferrer, }, } - : livePageData - const firstTouchAttributionProps = telemetryDataOverride - ? getFirstTouchAttributionProps(telemetryDataOverride) - : {} + : { ...livePageData, ph: { ...livePageData.ph } } + const firstTouchAttributionProps: Record = { + ...(telemetryDataOverride ? getFirstTouchAttributionProps(telemetryDataOverride) : {}), + } + + // --- First-referrer edge cookie handoff --- + // If the edge cookie has external context and the current referrer is internal, + // override the referrer so PostHog gets the real acquisition source. + const firstReferrerCookiePresent = Boolean(firstReferrerData) + let firstReferrerCookieConsumed = false + + if ( + firstReferrerData && + isExternalReferrer(firstReferrerData.referrer) && + !isExternalReferrer(pageData.ph.referrer) + ) { + pageData.ph.referrer = firstReferrerData.referrer + firstReferrerCookieConsumed = true + + // Prefer attribution context captured at the external entry point. + const { utms, click_ids, landing_url } = firstReferrerData + + Object.entries(utms).forEach(([key, value]) => { + const phKey = key.startsWith('utm_') ? `$${key}` : key + firstTouchAttributionProps[phKey] = value + }) + + Object.entries(click_ids).forEach(([key, value]) => { + firstTouchAttributionProps[key] = value + }) + + try { + const url = new URL(landing_url) + firstTouchAttributionProps.first_touch_url = url.href + firstTouchAttributionProps.first_touch_pathname = url.pathname + + if (url.search) { + firstTouchAttributionProps.first_touch_search = url.search + } else { + delete firstTouchAttributionProps.first_touch_search + } + } catch { + // Skip if landing URL is malformed + } + } const $referrer = pageData.ph.referrer const $referring_domain = (() => { @@ -170,6 +213,13 @@ function handlePageTelemetry( ...Object.fromEntries( Object.entries(featureFlags || {}).map(([k, v]) => [`$feature/${k}`, v]) ), + // Measurement properties for handoff observability + // Only included on the initial pageview (when firstReferrerData is explicitly + // passed as null or a value — subsequent pageviews leave it as undefined) + ...(firstReferrerData !== undefined && { + first_referrer_cookie_present: firstReferrerCookiePresent, + first_referrer_cookie_consumed: firstReferrerCookieConsumed, + }), }) } @@ -234,13 +284,13 @@ export const PageTelemetry = ({ const sendPageTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry( - API_URL, - pathnameRef.current, - featureFlagsRef.current, + return handlePageTelemetry({ + apiUrl: API_URL, + pathname: pathnameRef.current, + featureFlags: featureFlagsRef.current, slug, - ref - ).catch((e) => { + ref, + }).catch((e) => { console.error('Problem sending telemetry page:', e) }) }, [API_URL, enabled, hasAcceptedConsent, slug, ref]) @@ -283,6 +333,9 @@ export const PageTelemetry = ({ hasAcceptedConsent && !hasSentInitialPageTelemetryRef.current ) { + // Read the edge-set first-referrer cookie (cross-app handoff) + const firstReferrerData = parseFirstReferrerCookie(document.cookie) + const cookies = document.cookie.split(';') const telemetryCookieValue = cookies .map((cookie) => cookie.trim()) @@ -294,24 +347,39 @@ export const PageTelemetry = ({ const telemetryData = JSON.parse( decodeURIComponent(telemetryCookieValue) ) as SharedTelemetryData - handlePageTelemetry( - API_URL, - pathnameRef.current, - featureFlagsRef.current, + handlePageTelemetry({ + apiUrl: API_URL, + pathname: pathnameRef.current, + featureFlags: featureFlagsRef.current, slug, ref, - telemetryData - ) + telemetryDataOverride: telemetryData, + firstReferrerData, + }) } catch (error) { if (!IS_PROD) { console.warn('Invalid telemetry cookie data:', error) } - handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref) + handlePageTelemetry({ + apiUrl: API_URL, + pathname: pathnameRef.current, + featureFlags: featureFlagsRef.current, + slug, + ref, + firstReferrerData, + }) } finally { clearTelemetryDataCookie() } } else { - handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref) + handlePageTelemetry({ + apiUrl: API_URL, + pathname: pathnameRef.current, + featureFlags: featureFlagsRef.current, + slug, + ref, + firstReferrerData, + }) } hasSentInitialPageTelemetryRef.current = true diff --git a/packages/marketing/src/go/schemas.ts b/packages/marketing/src/go/schemas.ts index 5c879877f7886..540adff4efd9c 100644 --- a/packages/marketing/src/go/schemas.ts +++ b/packages/marketing/src/go/schemas.ts @@ -184,6 +184,8 @@ export const formSectionSchema = z.object({ disclaimer: z.string().optional(), /** Message shown after a successful submission. Defaults to a generic thank-you message. */ successMessage: z.string().optional(), + /** URL to redirect the user to after a successful submission. When set, overrides successMessage. */ + successRedirect: z.string().optional(), /** CRM integration config. When provided, form submissions are sent to the configured providers. */ crm: formCrmConfigSchema.optional(), }) diff --git a/packages/marketing/src/go/sections/FormSection.tsx b/packages/marketing/src/go/sections/FormSection.tsx index ba566d67998e1..8442d41825611 100644 --- a/packages/marketing/src/go/sections/FormSection.tsx +++ b/packages/marketing/src/go/sections/FormSection.tsx @@ -102,7 +102,11 @@ export default function FormSection({ section }: { section: GoFormSection }) { const result = await submitFormAction(section.crm, values, { pageUri, pageName }) if (result.success) { - setSubmitState('success') + if (section.successRedirect) { + window.location.href = section.successRedirect + } else { + setSubmitState('success') + } } else { setSubmitState('error') setErrorMessages(result.errors) diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index eca52e661700b..e145d04e445cf 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -298,5 +298,6 @@ export * from './src/components/Icon/icons/IconYoutubeSolid' // Export hooks export * from './src/lib/Hooks' +export * from './src/components/hooks/use-mobile' export * from './src/components/KeyboardShortcut/KeyboardShortcut' diff --git a/packages/ui/package.json b/packages/ui/package.json index 8e1909e2277f4..ddd2dc3cc0c97 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -72,7 +72,7 @@ "react-hook-form": "^7.45.0", "react-intersection-observer": "^9.8.2", "react-medium-image-zoom": "^5.2.4", - "react-resizable-panels": "^3.0.0", + "react-resizable-panels": "^4.6.5", "react-syntax-highlighter": "^15.6.6", "recharts": "catalog:", "sonner": "^1.5.0", diff --git a/packages/ui/src/components/shadcn/ui/resizable.tsx b/packages/ui/src/components/shadcn/ui/resizable.tsx index e36e712932dc0..d8ed45b9ac113 100644 --- a/packages/ui/src/components/shadcn/ui/resizable.tsx +++ b/packages/ui/src/components/shadcn/ui/resizable.tsx @@ -2,54 +2,115 @@ import { GripVertical } from 'lucide-react' import * as ResizablePrimitive from 'react-resizable-panels' -import { ImperativePanelHandle } from 'react-resizable-panels' import { cn } from '../../../lib/utils/cn' +// This is to avoid clashes with older versions of react-resizable-panels which have been saved in local storage. +const transformLayoutKey = (key: string) => + key.replace('react-resizable-panels:', 'react-resizable-panels-v4:') + +const serverCompatibleLocalStorage = { + getItem: (k: string) => { + if (typeof window === 'undefined') return null + const key = transformLayoutKey(k) + return localStorage.getItem(key) + }, + setItem: (k: string, value: string) => { + if (typeof window === 'undefined') return + const key = transformLayoutKey(k) + localStorage.setItem(key, value) + }, +} + +const ResizablePanelGroupWithPersistence = ({ + className, + autoSaveId, + defaultLayout: defaultLayoutProp, + onLayoutChanged: onLayoutChangedProp, + ...props +}: ResizablePrimitive.GroupProps & { autoSaveId: string }) => { + const { defaultLayout, onLayoutChanged } = useDefaultLayout({ + id: autoSaveId, + storage: serverCompatibleLocalStorage, + }) + + return ( + + ) +} + const ResizablePanelGroup = ({ className, + autoSaveId, ...props -}: React.ComponentProps) => ( - -) +}: ResizablePrimitive.GroupProps & { autoSaveId?: string }) => { + if (autoSaveId) { + return ( + + ) + } + + return ( + + ) +} -const ResizablePanel = ResizablePrimitive.Panel +const ResizablePanel = ({ ...props }: ResizablePrimitive.PanelProps) => { + return +} const ResizableHandle = ({ withHandle, className, ...props -}: React.ComponentProps & { +}: ResizablePrimitive.SeparatorProps & { withHandle?: boolean -}) => ( - div]:rotate-90', - 'data-[resize-handle-state=drag]:bg-border-strong', - 'group', - 'transition-colors', - className - )} - {...props} - > - {withHandle && ( -
- -
- )} -
-) - -export { ResizableHandle, ResizablePanel, ResizablePanelGroup, type ImperativePanelHandle } +}) => { + return ( + div]:rotate-90', + 'data-[separator=active]:bg-border-strong', + 'group', + 'transition-colors', + className + )} + style={{ cursor: 'auto' }} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+ ) +} + +const useDefaultLayout = ResizablePrimitive.useDefaultLayout +const usePanelRef = ResizablePrimitive.usePanelRef + +export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useDefaultLayout, usePanelRef } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83120c44bc336..5363c8433b790 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1218,6 +1218,9 @@ importers: eslint-plugin-barrel-files: specifier: ^2.0.7 version: 2.0.7(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + eslint-plugin-jsx-a11y: + specifier: ^6.10.2 + version: 6.10.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) graphql-ws: specifier: 5.14.1 version: 5.14.1(graphql@16.11.0) @@ -2481,8 +2484,8 @@ importers: specifier: ^5.2.4 version: 5.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-resizable-panels: - specifier: ^3.0.0 - version: 3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^4.6.5 + version: 4.6.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-syntax-highlighter: specifier: ^15.6.6 version: 15.6.6(react@18.3.1) @@ -16019,11 +16022,11 @@ packages: '@types/react': optional: true - react-resizable-panels@3.0.6: - resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + react-resizable-panels@4.6.5: + resolution: {integrity: sha512-pmQP6qv9KmsesNMvWVNvVfVJAwYSOWWbAOAtrPR8Cre20+j1NWIlyft0btjtDQE+OepXmI6g3VPrCXQY0oD7+Q==} peerDependencies: - react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 react-resizable@3.0.5: resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} @@ -17284,6 +17287,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me termi-link@1.1.0: resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} @@ -35071,7 +35075,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-resizable-panels@4.6.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aa4e736d669e0..ff4ee79cb1e48 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,6 +59,7 @@ minimumReleaseAgeExclude: - lodash-es - lodash - next-mdx-remote + - react-resizable-panels onlyBuiltDependencies: - supabase