From 02b994e59fd8122d58b4dcdbdc4bb1eaa52fc106 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Fri, 6 Feb 2026 10:28:50 -0700 Subject: [PATCH 01/22] docs(mcp): document tools, feature groups, and configuration options (#42528) Adds more MCP docs on which tools, features, options, etc are available on the Supabase MCP server. Currently these only live in the [repo README](https://github.com/supabase-community/supabase-mcp), so this adds them to the official docs which many folks land on first. ### Preview https://docs-git-docs-mcp-tools-and-options-supabase.vercel.app/docs/guides/getting-started/mcp Resolves AI-357 ## Summary by CodeRabbit * **Documentation** * Added an "Available tools" section documenting MCP tool groups and their available commands * Updated internal documentation anchors to improve navigation between related configuration topics --- .../content/guides/getting-started/mcp.mdx | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index 30addfc7514..3a6c8982a94 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -30,6 +30,86 @@ To verify the client has access to the MCP server tools, try asking it to query For curated, ready-to-use prompts that work well with IDEs and AI agents, see our [AI Prompts](/guides/getting-started/ai-prompts) collection. +## Available tools + +The Supabase MCP server provides tools organized into feature groups. All groups except Storage are enabled by default. You can enable or disable specific groups using the [configuration panel above](#step-2-configure-your-ai-tool). + +### Database + +- `list_tables` - List all tables in the database +- `list_extensions` - List available/installed Postgres extensions +- `list_migrations` - List database migrations +- `apply_migration` - Apply a database migration +- `execute_sql` - Execute SQL queries + +### Debugging + +- `get_logs` - Retrieve service logs (API, Postgres, Edge Functions, Auth, Storage, Realtime) +- `get_advisors` - Get security and performance advisors + +### Development + +- `get_project_url` - Get the API URL for a project +- `get_publishable_keys` - Get anon/public keys +- `generate_typescript_types` - Generate TypeScript types from schema + +### Edge Functions + +- `list_edge_functions` - List all Edge Functions +- `get_edge_function` - Get a specific Edge Function +- `deploy_edge_function` - Deploy an Edge Function + +### Account management + + + +Disabled when using project-scoped mode (`project_ref` parameter). + + + +- `list_projects` / `get_project` - List or get project details +- `create_project` / `pause_project` / `restore_project` - Manage projects +- `list_organizations` / `get_organization` - Organization management +- `get_cost` / `confirm_cost` - Cost information + +### Docs + +- `search_docs` - Search Supabase documentation + +### Branching (experimental) + + + +Requires a paid plan. + + + +- `create_branch` / `list_branches` / `delete_branch` - Branch management +- `merge_branch` / `reset_branch` / `rebase_branch` - Branch operations + +### Storage (disabled by default) + +- `list_storage_buckets` - List storage buckets +- `get_storage_config` / `update_storage_config` - Storage configuration + +## Configuration options + +The [configuration panel above](#step-2-configure-your-ai-tool) can set these options for you. If you prefer to configure manually, the following URL query parameters are available: + +| Parameter | Description | Example | +| ------------------- | ---------------------------------------------------- | ------------------------- | +| `read_only=true` | Execute all queries as a read-only Postgres user | `?read_only=true` | +| `project_ref=` | Scope to a specific project (disables account tools) | `?project_ref=abc123` | +| `features=` | Enable only specific tool groups (comma-separated) | `?features=database,docs` | + +Parameters can be combined: `https://mcp.supabase.com/mcp?project_ref=abc123&read_only=true` + + + +When using [Supabase CLI](/docs/guides/cli) for local development, the MCP server is available at `http://localhost:54321/mcp`. + + + ## Manual authentication By default the hosted Supabase MCP server uses [dynamic client registration](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration) to authenticate with your Supabase org. This means that you don't need to manually create a personal access token (PAT) or OAuth app to use the server. @@ -108,10 +188,10 @@ We recommend the following best practices to mitigate security risks when using - **Don't connect to production**: Use the MCP server with a development project, not production. LLMs are great at helping design and test applications, so leverage them in a safe environment without exposing real data. Be sure that your development environment contains non-production data (or obfuscated data). - **Don't give to your customers**: The MCP server operates under the context of your developer permissions, so you should not give it to your customers or end users. Instead, use it internally as a developer tool to help you build and test your applications. -- **Read-only mode**: If you must connect to real data, set the server to [read-only](https://github.com/supabase-community/supabase-mcp#read-only-mode) mode, which executes all queries as a read-only Postgres user. -- **Project scoping**: Scope your MCP server to a [specific project](https://github.com/supabase-community/supabase-mcp#project-scoped-mode), limiting access to only that project's resources. This prevents LLMs from accessing data from other projects in your Supabase account. +- **Read-only mode**: If you must connect to real data, set the server to [read-only](#configuration-options) mode, which executes all queries as a read-only Postgres user. +- **Project scoping**: Scope your MCP server to a [specific project](#configuration-options), limiting access to only that project's resources. This prevents LLMs from accessing data from other projects in your Supabase account. - **Branching**: Use Supabase's [branching feature](/docs/guides/deployment/branching) to create a development branch for your database. This allows you to test changes in a safe environment before merging them to production. -- **Feature groups**: The server allows you to enable or disable specific [tool groups](https://github.com/supabase-community/supabase-mcp#feature-groups), so you can control which tools are available to the LLM. This helps reduce the attack surface and limits the actions that LLMs can perform to only those that you need. +- **Feature groups**: Restrict which [tool groups](#available-tools) are available using the `features` [configuration option](#configuration-options). This helps reduce the attack surface and limits the actions that LLMs can perform to only those that you need. ## On GitHub From ba0f5d6e7683a2248ec9ebbe1e1ea3deb010f2ae Mon Sep 17 00:00:00 2001 From: Sean Oliver <882952+seanoliver@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:42:01 -0800 Subject: [PATCH 02/22] fix(telemetry): add cookie fallback to getDistinctId for identity resolution (#42535) --- packages/common/posthog-client.ts | 50 +++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/common/posthog-client.ts b/packages/common/posthog-client.ts index cd7035116d2..2f0c3fcf828 100644 --- a/packages/common/posthog-client.ts +++ b/packages/common/posthog-client.ts @@ -178,17 +178,55 @@ class PostHogClient { /** * Returns PostHog's distinct_id, which holds first-touch attribution data. - * Returns undefined until PostHog's `loaded` callback fires. + * Falls back to reading from PostHog cookie if SDK isn't initialized yet + * (e.g., immediately after OAuth redirect before PostHog loads). */ getDistinctId(): string | undefined { - if (!this.initialized) return undefined + if (this.initialized) { + try { + return posthog.get_distinct_id() + } catch (error) { + console.error('PostHog getDistinctId failed:', error) + } + } + + // Fallback: parse distinct_id from PostHog cookie + return this.getDistinctIdFromCookie() + } + + /** + * Parse distinct_id from PostHog cookie. + * PostHog stores data in a cookie named `ph__posthog` with format: + * { distinct_id: "...", ... } + */ + private getDistinctIdFromCookie(): string | undefined { + if (typeof document === 'undefined') return undefined try { - return posthog.get_distinct_id() - } catch (error) { - console.error('PostHog getDistinctId failed:', error) - return undefined + const cookieName = `ph_${this.config.apiKey}_posthog` + const cookies = document.cookie.split(';') + + for (const cookie of cookies) { + const trimmed = cookie.trim() + const eqIndex = trimmed.indexOf('=') + if (eqIndex === -1) continue + + const name = trimmed.substring(0, eqIndex) + if (name !== cookieName) continue + + // Use substring instead of split to handle '=' chars in the value + const cookieValue = decodeURIComponent(trimmed.substring(eqIndex + 1)) + const phData = JSON.parse(cookieValue) + + if (phData.distinct_id && typeof phData.distinct_id === 'string') { + return phData.distinct_id + } + } + } catch { + // No op, cookie may not exist (first visit) or be malformed } + + return undefined } /** From c7f653f33f02cfe459e9970a42d98e5626d176bb Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 6 Feb 2026 10:50:41 -0700 Subject: [PATCH 03/22] chore: move api solution to storage and parallelize tests (#42565) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? - Move storage tests to run in parallel - Updated utils to use env ## Summary by CodeRabbit * **Tests** * Improved test reliability by shifting storage bucket setup and cleanup from UI-based to API-backed operations. * Enhanced test isolation with streamlined prerequisite navigation steps. * **Chores** * Updated environment configuration to support dynamic API URL and service role key settings. * Refactored internal storage management utilities for improved maintainability. --- e2e/studio/env.config.ts | 8 ++- e2e/studio/features/storage.spec.ts | 107 +++++++++++++++++----------- e2e/studio/utils/db/client.ts | 9 +-- e2e/studio/utils/storage/client.ts | 39 ++++++++++ e2e/studio/utils/storage/index.ts | 2 + e2e/studio/utils/storage/queries.ts | 54 ++++++++++++++ 6 files changed, 170 insertions(+), 49 deletions(-) create mode 100644 e2e/studio/utils/storage/client.ts create mode 100644 e2e/studio/utils/storage/index.ts create mode 100644 e2e/studio/utils/storage/queries.ts diff --git a/e2e/studio/env.config.ts b/e2e/studio/env.config.ts index 9999ac4b0c7..b22d469a5eb 100644 --- a/e2e/studio/env.config.ts +++ b/e2e/studio/env.config.ts @@ -13,9 +13,12 @@ const toBoolean = (value?: string) => { return normalized === 'true' } +// Default service role key for local development +const DEFAULT_SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" + export const env = { STUDIO_URL: process.env.STUDIO_URL || 'http://localhost:8082', - API_URL: process.env.API_URL || 'https://localhost:8080', + API_URL: process.env.API_URL || 'http://127.0.0.1:54321', IS_PLATFORM: toBoolean(process.env.IS_PLATFORM || 'false'), EMAIL: process.env.EMAIL, @@ -40,6 +43,9 @@ export const env = { IS_APP_RUNNING_ON_LOCALHOST: process.env.STUDIO_URL?.includes('localhost') || process.env.STUDIO_URL?.includes('127.0.0.1'), + + + SERVICE_ROLE_KEY: process.env.SERVICE_ROLE_KEY || DEFAULT_SERVICE_ROLE_KEY, } export const STORAGE_STATE_PATH = path.join(import.meta.dirname, './playwright/.auth/user.json') diff --git a/e2e/studio/features/storage.spec.ts b/e2e/studio/features/storage.spec.ts index c4c24895028..b2d371b9de9 100644 --- a/e2e/studio/features/storage.spec.ts +++ b/e2e/studio/features/storage.spec.ts @@ -6,21 +6,24 @@ import { waitForApiResponse } from '../utils/wait-for-response.js' import { createBucket, createFolder, - deleteAllBuckets, deleteBucket, deleteItem, downloadFile, navigateToBucket, + navigateToStorageFiles, renameItem, uploadFile, } from '../utils/storage-helpers.js' -import { dismissToastsIfAny } from '../utils/dismiss-toast.js' +import { + createBucket as createBucketViaApi, + deleteBucket as deleteBucketViaApi, +} from '../utils/storage/index.js' const bucketNamePrefix = 'pw_bucket' -test.describe.serial('Storage', () => { +test.describe('Storage', () => { test.beforeEach(async ({ page, ref }) => { - await deleteAllBuckets(page, ref) + await navigateToStorageFiles(page, ref) }) test('can navigate to storage page', async ({ page, ref }) => { @@ -36,6 +39,7 @@ test.describe.serial('Storage', () => { test('can create a private bucket', async ({ page, ref }) => { const bucketName = `${bucketNamePrefix}_private` + await deleteBucketViaApi(bucketName) await createBucket(page, ref, bucketName, false) // Verify it's marked as private (no "Public" badge should be visible) @@ -50,6 +54,7 @@ test.describe.serial('Storage', () => { test('can create a public bucket', async ({ page, ref }) => { const bucketName = `${bucketNamePrefix}_public` + await deleteBucketViaApi(bucketName) await createBucket(page, ref, bucketName, true) // Verify it's marked as public - wait for the badge to appear @@ -66,8 +71,10 @@ test.describe.serial('Storage', () => { test('can edit bucket settings', async ({ page, ref }) => { const bucketName = `${bucketNamePrefix}_edit` - // Create a private bucket - await createBucket(page, ref, bucketName, false) + // Create a fresh private bucket via API + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) // Navigate to the bucket await navigateToBucket(page, ref, bucketName) @@ -96,12 +103,14 @@ test.describe.serial('Storage', () => { }) test('can delete a bucket', async ({ page, ref }) => { - const bucketName = `${bucketNamePrefix}_delete` + const bucketName = `${bucketNamePrefix}_delbkt` - // Create a bucket - await createBucket(page, ref, bucketName, false) + // Create a bucket via API + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) - // Delete it + // Delete it via UI await deleteBucket(page, ref, bucketName) // Verify it's gone @@ -115,10 +124,12 @@ test.describe.serial('Storage', () => { const bucketName1 = `${bucketNamePrefix}_search_1` const bucketName2 = `${bucketNamePrefix}_search_2` - // Create two buckets - await createBucket(page, ref, bucketName1, false) - await dismissToastsIfAny(page) - await createBucket(page, ref, bucketName2, false) + // Create two buckets via API + await deleteBucketViaApi(bucketName1) + await deleteBucketViaApi(bucketName2) + await createBucketViaApi(bucketName1, false) + await createBucketViaApi(bucketName2, false) + await navigateToStorageFiles(page, ref) // Search for first bucket const searchInput = page.getByPlaceholder('Search for a bucket') @@ -152,24 +163,25 @@ test.describe.serial('Storage', () => { const bucketName = `${bucketNamePrefix}_upload` const fileName = 'test-file.txt' - // Create a bucket and navigate to it - await createBucket(page, ref, bucketName, false) + // Create a bucket via API and navigate to it + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) // Upload a file const filePath = path.join(import.meta.dirname, 'files', fileName) await uploadFile(page, filePath, fileName) - - // Clean up - await deleteBucket(page, ref, bucketName) }) test('can create a folder', async ({ page, ref }) => { - const bucketName = `${bucketNamePrefix}_folder` + const bucketName = `${bucketNamePrefix}_newfolder` const folderName = 'test_folder' - // Create a bucket and navigate to it - await createBucket(page, ref, bucketName, false) + // Create a bucket via API and navigate to it + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) // Create a folder @@ -181,8 +193,10 @@ test.describe.serial('Storage', () => { const fileName = 'test-file.txt' const newFileName = 'renamed-file.txt' - // Create a bucket, navigate to it, and upload a file - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and upload a file + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) const filePath = path.join(import.meta.dirname, 'files', fileName) @@ -190,18 +204,17 @@ test.describe.serial('Storage', () => { // Rename the file await renameItem(page, fileName, newFileName) - - // Clean up - await deleteBucket(page, ref, bucketName) }) test('can rename a folder', async ({ page, ref }) => { - const bucketName = `${bucketNamePrefix}_rename_folder` + const bucketName = `${bucketNamePrefix}_mvdir` const folderName = 'old_folder' const newFolderName = 'new_folder' - // Create a bucket, navigate to it, and create a folder - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and create a folder + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) await createFolder(page, folderName) @@ -210,11 +223,13 @@ test.describe.serial('Storage', () => { }) test('resets folder name when renaming with empty string', async ({ page, ref }) => { - const bucketName = `${bucketNamePrefix}_rename_folder_empty` + const bucketName = `${bucketNamePrefix}_reset_enter` const folderName = 'folder_to_rename' - // Create a bucket, navigate to it, and create a folder - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and create a folder + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) await createFolder(page, folderName) @@ -243,11 +258,13 @@ test.describe.serial('Storage', () => { }) test('resets folder name when clicking outside with empty string', async ({ page, ref }) => { - const bucketName = `${bucketNamePrefix}_rename_folder_blur` + const bucketName = `${bucketNamePrefix}_reset_blur` const folderName = 'folder_to_blur' - // Create a bucket, navigate to it, and create a folder - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and create a folder + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) await createFolder(page, folderName) @@ -278,8 +295,10 @@ test.describe.serial('Storage', () => { const bucketName = `${bucketNamePrefix}_delete_file` const fileName = 'test-file.txt' - // Create a bucket, navigate to it, and upload a file - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and upload a file + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) const filePath = path.join(import.meta.dirname, 'files', fileName) @@ -293,8 +312,10 @@ test.describe.serial('Storage', () => { const bucketName = `${bucketNamePrefix}_delete_folder` const folderName = 'test_folder' - // Create a bucket, navigate to it, and create a folder - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and create a folder + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) await createFolder(page, folderName) @@ -306,8 +327,10 @@ test.describe.serial('Storage', () => { const bucketName = `${bucketNamePrefix}_download` const fileName = 'test-file.txt' - // Create a bucket, navigate to it, and upload a file - await createBucket(page, ref, bucketName, false) + // Create a bucket via API, navigate to it, and upload a file + await deleteBucketViaApi(bucketName) + await createBucketViaApi(bucketName, false) + await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) const filePath = path.join(import.meta.dirname, 'files', fileName) diff --git a/e2e/studio/utils/db/client.ts b/e2e/studio/utils/db/client.ts index f8f1c0689fb..fb8c584bc62 100644 --- a/e2e/studio/utils/db/client.ts +++ b/e2e/studio/utils/db/client.ts @@ -1,7 +1,4 @@ -// Default Supabase CLI constants (hardcoded for local development) -const SERVICE_ROLE_KEY = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' -const API_URL = 'http://127.0.0.1:54321' +import { env } from "../../env.config.js" /** * Execute a SQL query against the local Supabase database via pg-meta. @@ -15,11 +12,11 @@ const API_URL = 'http://127.0.0.1:54321' * @throws Error if the query fails */ export async function query(sql: string, params?: Array): Promise> { - const response = await fetch(`${API_URL}/pg/query`, { + const response = await fetch(`${env.API_URL}/pg/query`, { method: 'POST', headers: { 'Content-Type': 'application/json', - apikey: SERVICE_ROLE_KEY, + apikey: env.SERVICE_ROLE_KEY, }, body: JSON.stringify({ query: sql, parameters: params }), }) diff --git a/e2e/studio/utils/storage/client.ts b/e2e/studio/utils/storage/client.ts new file mode 100644 index 00000000000..208d6edfd0f --- /dev/null +++ b/e2e/studio/utils/storage/client.ts @@ -0,0 +1,39 @@ +import { env } from "../../env.config.js"; + +/** + * Make an HTTP request to the local Supabase Storage API. + * + * @param path - The path to append to the storage base URL (e.g., '/bucket') + * @param options - Optional method and body + * @returns Parsed JSON response + * @throws Error if the request fails + */ +export async function storageRequest( + path: string, + options?: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: Record } +): Promise { + const storageUrl = `${env.API_URL}/storage/v1` + + const headers: Record = { + apikey: env.SERVICE_ROLE_KEY, + Authorization: `Bearer ${env.SERVICE_ROLE_KEY}`, + } + + if (options?.body) { + headers['Content-Type'] = 'application/json' + } + + const response = await fetch(`${storageUrl}${path}`, { + method: options?.method ?? 'GET', + headers, + body: options?.body ? JSON.stringify(options.body) : undefined, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`Storage request failed (${response.status}): ${text}`) + } + + const text = await response.text() + return text ? JSON.parse(text) : ({} as T) +} diff --git a/e2e/studio/utils/storage/index.ts b/e2e/studio/utils/storage/index.ts new file mode 100644 index 00000000000..cffa68bfd69 --- /dev/null +++ b/e2e/studio/utils/storage/index.ts @@ -0,0 +1,2 @@ +export { storageRequest } from './client.js' +export { createBucket, deleteBucket, deleteAllBuckets, listBuckets } from './queries.js' diff --git a/e2e/studio/utils/storage/queries.ts b/e2e/studio/utils/storage/queries.ts new file mode 100644 index 00000000000..e518b8a3381 --- /dev/null +++ b/e2e/studio/utils/storage/queries.ts @@ -0,0 +1,54 @@ +import { storageRequest } from './client.js' + +interface Bucket { + id: string + name: string + public: boolean +} + +/** + * List all storage buckets. + */ +export async function listBuckets(): Promise { + return storageRequest('/bucket') +} + +/** + * Create a storage bucket. Idempotent — skips creation if the bucket already exists. + * + * @param name - Bucket name / id + * @param isPublic - Whether the bucket should be public (default: false) + */ +export async function createBucket(name: string, isPublic: boolean = false): Promise { + const buckets = await listBuckets() + if (buckets.some((b) => b.id === name)) return + + await storageRequest('/bucket', { + method: 'POST', + body: { id: name, name, public: isPublic }, + }) +} + +/** + * Delete a storage bucket. Idempotent — empties the bucket first, then deletes it. + * No-ops if the bucket does not exist. + * + * @param name - Bucket name / id + */ +export async function deleteBucket(name: string): Promise { + const buckets = await listBuckets() + if (!buckets.some((b) => b.id === name)) return + + await storageRequest(`/bucket/${name}/empty`, { method: 'POST' }) + await storageRequest(`/bucket/${name}`, { method: 'DELETE' }) +} + +/** + * Delete every storage bucket. + */ +export async function deleteAllBuckets(): Promise { + const buckets = await listBuckets() + for (const bucket of buckets) { + await deleteBucket(bucket.id) + } +} From 6bbe2c3297cfa57c5038b0713c350cc1e4d6d7af Mon Sep 17 00:00:00 2001 From: Sean Oliver <882952+seanoliver@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:46:53 -0800 Subject: [PATCH 04/22] feat(telemetry): add dev telemetry toolbar (#42259) ## Problem We need a local-only UI to inspect client and server telemetry events and override flags during development without touching non-local env behavior. This work is intended to be shared across Studio, Docs, and WWW. ## Changes - Introduced a shared `dev-tools` package with the Dev Telemetry Toolbar UI, trigger, and provider. - Wired the toolbar into Studio, Docs, and WWW app shells (local-only gating). - Added a local-only `devTelemetry()` opt-in with storage gating and SSE subscription. - Wired client PostHog events into a local listener and re-exported types. - Added local flag override cookie support in the UI and CODEOWNERS for the new package. - Added unit tests covering local/non-local behavior and flag utilities. ## Testing Manual (local only): - Start each app locally: `pnpm dev:studio`, `pnpm dev:docs`, `pnpm dev:www` - Open the app, run `devTelemetry()` in the browser console - Click around and confirm both client and server events appear (client will be page views only) - Verify feature flag overrides (PostHog + ConfigCat) persist and restore correctly - Confirm dismissing the toolbar clears local storage and hides the trigger Unblocked by https://github.com/supabase/platform/pull/29172 Resolves GROWTH-591 Demo: [github.com/user-attachments/assets/60b376db-7440-4ada-82f5-d1bd4af4db3b](https://github.com/user-attachments/assets/60b376db-7440-4ada-82f5-d1bd4af4db3b) Screenshots: 1 2 3 4 5 6 ## Summary by CodeRabbit * **New Features** * Developer Toolbar (local only): view client/server telemetry, inspect events, and manage/override feature flags with persistent overrides, filtering, and clear/reload. * Client-side telemetry hooks: surface structured events to dev tooling for realtime inspection. * **Bug Fixes** * Fixed end-of-file newline in shared code. * **Chores** * Added dev-tools package, integrated provider and trigger across Studio, Docs, and marketing sites, and added CODEOWNERS entry. * **Tests** * Added comprehensive tests and test setup for the DevToolbar. --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .github/CODEOWNERS | 3 +- .../Navigation/NavigationMenu/TopNavBar.tsx | 2 + apps/docs/features/app.providers.tsx | 36 +- apps/docs/package.json | 1 + .../LayoutHeader/LayoutHeader.tsx | 3 +- apps/studio/package.json | 1 + apps/studio/pages/_app.tsx | 53 ++- apps/www/app/providers.tsx | 38 +- apps/www/components/Nav/index.tsx | 2 + apps/www/package.json | 1 + apps/www/pages/_app.tsx | 42 +- packages/common/feature-flags.tsx | 39 +- packages/common/posthog-client.ts | 56 +++ packages/common/telemetry.tsx | 4 +- packages/dev-tools/DevToolbar.test.tsx | 386 +++++++++++++++ packages/dev-tools/DevToolbar.tsx | 438 ++++++++++++++++++ packages/dev-tools/DevToolbarContext.tsx | 212 +++++++++ packages/dev-tools/DevToolbarTrigger.tsx | 113 +++++ packages/dev-tools/index.ts | 36 ++ packages/dev-tools/package.json | 39 ++ packages/dev-tools/tsconfig.json | 9 + packages/dev-tools/types.ts | 35 ++ packages/dev-tools/utils.ts | 90 ++++ packages/dev-tools/vitest.config.ts | 14 + packages/dev-tools/vitest.setup.ts | 25 + pnpm-lock.yaml | 59 +++ 26 files changed, 1655 insertions(+), 82 deletions(-) create mode 100644 packages/dev-tools/DevToolbar.test.tsx create mode 100644 packages/dev-tools/DevToolbar.tsx create mode 100644 packages/dev-tools/DevToolbarContext.tsx create mode 100644 packages/dev-tools/DevToolbarTrigger.tsx create mode 100644 packages/dev-tools/index.ts create mode 100644 packages/dev-tools/package.json create mode 100644 packages/dev-tools/tsconfig.json create mode 100644 packages/dev-tools/types.ts create mode 100644 packages/dev-tools/utils.ts create mode 100644 packages/dev-tools/vitest.config.ts create mode 100644 packages/dev-tools/vitest.setup.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a355b309cea..1d631f4a7dc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,7 @@ /packages/shared-data/pricing.ts @supabase/billing /packages/shared-data/plans.ts @supabase/billing /packages/common/telemetry-constants.ts @supabase/growth-eng +/packages/dev-tools/ @supabase/growth-eng /packages/pg-meta @supabase/postgres @avallete /packages/ui-patterns @supabase/design @@ -22,4 +23,4 @@ /apps/studio/data/sql/queries/ @supabase/postgres @avallete -/packages/shared-data/compute-disk-limits.ts @supabase/infra \ No newline at end of file +/packages/shared-data/compute-disk-limits.ts @supabase/infra diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx index c56f34d0fe9..c88b9525ed5 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx @@ -8,6 +8,7 @@ import { memo, useState } from 'react' import { useIsLoggedIn, useIsUserLoading, useUser } from 'common' import { isFeatureEnabled } from 'common/enabled-features' +import { DevToolbarTrigger } from 'dev-tools' import { Button, buttonVariants, cn } from 'ui' import { AuthenticatedDropdownMenu, CommandMenuTriggerInput } from 'ui-patterns' import { getCustomContent } from '../../../lib/custom-content/getCustomContent' @@ -43,6 +44,7 @@ const TopNavBar: FC = () => {
+ diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 99e73b93c98..6d6caf07877 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -1,6 +1,7 @@ import type { PropsWithChildren } from 'react' import { FeatureFlagProvider, IS_PLATFORM, ThemeProvider } from 'common' +import { DevToolbar, DevToolbarProvider } from 'dev-tools' import { SonnerToaster, TooltipProvider } from 'ui' import SiteLayout from '~/layouts/SiteLayout' import { API_URL } from '~/lib/constants' @@ -19,22 +20,25 @@ function GlobalProviders({ children }: PropsWithChildren) { - - - - - -
- - {children} - - - -
-
- -
-
+ + + + + + +
+ + {children} + + + +
+
+ + +
+
+
diff --git a/apps/docs/package.json b/apps/docs/package.json index a3d5c5914c1..4e4ab01a5fe 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -63,6 +63,7 @@ "common": "workspace:*", "common-tags": "^1.8.2", "config": "workspace:*", + "dev-tools": "workspace:*", "eslint-config-supabase": "workspace:*", "framer-motion": "^11.0.3", "github-slugger": "^2.0.0", diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index fb17a705f34..6f663be1e76 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -22,7 +22,7 @@ import { ReactNode, useMemo } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { Badge, cn } from 'ui' import { CommandMenuTriggerInput } from 'ui-patterns' - +import { DevToolbarTrigger } from 'dev-tools' import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown' import { HelpPopover } from './HelpPopover' @@ -215,6 +215,7 @@ export const LayoutHeader = ({ {customHeaderComponents && customHeaderComponents} {IS_PLATFORM ? ( <> +
diff --git a/apps/studio/package.json b/apps/studio/package.json index 68d43465df2..49615234eec 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -84,6 +84,7 @@ "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", "dnd-core": "^16.0.1", diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 3156d1c20c0..5875b41e480 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -23,22 +23,11 @@ import { loader } from '@monaco-editor/react' import * as Sentry from '@sentry/nextjs' import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import dayjs from 'dayjs' -import customParseFormat from 'dayjs/plugin/customParseFormat' -import duration from 'dayjs/plugin/duration' -import relativeTime from 'dayjs/plugin/relativeTime' -import timezone from 'dayjs/plugin/timezone' -import utc from 'dayjs/plugin/utc' -import Head from 'next/head' -import { NuqsAdapter } from 'nuqs/adapters/next/pages' -import { type ComponentProps, ErrorInfo, useCallback } from 'react' -import { ErrorBoundary } from 'react-error-boundary' - import { FeatureFlagProvider, - getFlags, TelemetryTagManager, ThemeProvider, + getFlags, useThemeSandbox, } from 'common' import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router' @@ -49,8 +38,15 @@ import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/Featur import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider' import { RouteValidationWrapper } from 'components/interfaces/App/RouteValidationWrapper' import { MainScrollContainerProvider } from 'components/layouts/MainScrollContainerContext' +import { DevToolbar, DevToolbarProvider } from 'dev-tools' import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErrorBoundaryState' import { useRootQueryClient } from 'data/query-client' +import dayjs from 'dayjs' +import customParseFormat from 'dayjs/plugin/customParseFormat' +import duration from 'dayjs/plugin/duration' +import relativeTime from 'dayjs/plugin/relativeTime' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' import { customFont, sourceCodePro } from 'fonts' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -58,6 +54,10 @@ import { AuthProvider } from 'lib/auth' import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants' import { ProfileProvider } from 'lib/profile' import { Telemetry } from 'lib/telemetry' +import Head from 'next/head' +import { NuqsAdapter } from 'nuqs/adapters/next/pages' +import { type ComponentProps, ErrorInfo, useCallback } from 'react' +import { ErrorBoundary } from 'react-error-boundary' import { AiAssistantStateContextProvider } from 'state/ai-assistant-state' import type { AppPropsWithLayout } from 'types' import { SonnerToaster, TooltipProvider } from 'ui' @@ -173,19 +173,22 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { enableSystem disableTransitionOnChange > - - - - - {getLayout()} - - - - - - - - + + + + + + {getLayout()} + + + + + + + + + + diff --git a/apps/www/app/providers.tsx b/apps/www/app/providers.tsx index 298b3b390ea..663b4d9c206 100644 --- a/apps/www/app/providers.tsx +++ b/apps/www/app/providers.tsx @@ -11,6 +11,7 @@ import { useThemeSandbox, } from 'common' import { WwwCommandMenu } from 'components/CommandMenu' +import { DevToolbar, DevToolbarProvider } from 'dev-tools' import { API_URL } from 'lib/constants' import { themes, TooltipProvider, SonnerToaster } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' @@ -25,21 +26,28 @@ function Providers({ children }: { children: React.ReactNode }) { - t.value)} enableSystem disableTransitionOnChange> - - - - - {children} - - - - - + + t.value)} + enableSystem + disableTransitionOnChange + > + + + + + {children} + + + + + + + diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index ce02b7cd8bc..01ed84f4cc0 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -6,6 +6,7 @@ import React, { useState } from 'react' import { useWindowSize } from 'react-use' import { useIsLoggedIn, useIsUserLoading, useUser } from 'common' +import { DevToolbarTrigger } from 'dev-tools' import { Button, buttonVariants, cn } from 'ui' import { AuthenticatedDropdownMenu } from 'ui-patterns' @@ -155,6 +156,7 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => { isUserLoading ? 'opacity-0' : 'opacity-100' )} > + {isLoggedIn ? ( <> diff --git a/apps/www/package.json b/apps/www/package.json index a2432a74126..15905a8a84b 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -45,6 +45,7 @@ "common-tags": "^1.8.2", "config": "workspace:*", "dayjs": "^1.11.12", + "dev-tools": "workspace:*", "eslint-config-supabase": "workspace:*", "framer-motion": "^11.0.3", "globby": "^13.2.2", diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index 55a845f09b0..3b043b098fd 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -11,6 +11,7 @@ import { ThemeProvider, useThemeSandbox, } from 'common' +import { DevToolbar, DevToolbarProvider } from 'dev-tools' import { DefaultSeo } from 'next-seo' import type { AppProps } from 'next/app' import Head from 'next/head' @@ -90,25 +91,28 @@ export default function App({ Component, pageProps }: AppProps) { {/* [TODO] I think we need to deconflict with the providers in layout.tsx? */} - theme.value)} - enableSystem - disableTransitionOnChange - forcedTheme={forceDarkMode ? 'dark' : undefined} - > - - - - - - - - - + + theme.value)} + enableSystem + disableTransitionOnChange + forcedTheme={forceDarkMode ? 'dark' : undefined} + > + + + + + + + + + + + diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index 7a82f84a5e6..cbaaba96c9c 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -163,17 +163,50 @@ export const FeatureFlagProvider = ({ : Promise.resolve([]), ]) + const isLocalDev = process.env.NODE_ENV === 'development' + + const safeParse = ( + value: string | undefined + ): Record => { + if (!value) return {} + try { + return JSON.parse(value) + } catch { + return {} + } + } + // Process PostHog flags if loaded if (Object.keys(flags).length > 0) { - flagStore.posthog = flags + // Apply local dev overrides for PostHog flags + if (isLocalDev) { + try { + const cookies = getCookies() + const phOverrides = safeParse(cookies['x-ph-flag-overrides']) + flagStore.posthog = { ...flags, ...phOverrides } + } catch { + flagStore.posthog = flags + } + } else { + flagStore.posthog = flags + } } // Process ConfigCat flags if loaded if (flagValues.length > 0) { - let overridesCookieValue: Record = {} + let overridesCookieValue: Record = {} + try { const cookies = getCookies() - overridesCookieValue = JSON.parse(cookies['vercel-flag-overrides']) + // Merge overrides: vercel-flag-overrides first, then x-cc-flag-overrides (local only) + // x-cc-flag-overrides takes precedence in local dev + const vercelOverrides = safeParse(cookies['vercel-flag-overrides']) + const ccOverrides = isLocalDev ? safeParse(cookies['x-cc-flag-overrides']) : {} + + overridesCookieValue = { + ...vercelOverrides, + ...ccOverrides, // local overrides take precedence + } } catch {} flagValues.forEach((item) => { diff --git a/packages/common/posthog-client.ts b/packages/common/posthog-client.ts index 2f0c3fcf828..24284840a02 100644 --- a/packages/common/posthog-client.ts +++ b/packages/common/posthog-client.ts @@ -4,6 +4,17 @@ import posthog, { PostHogConfig } from 'posthog-js' // (e.g. if a user navigates around a lot before accepting consent) const MAX_PENDING_EVENTS = 20 +export interface ClientTelemetryEvent { + id: string + timestamp: number + eventType: 'capture' | 'identify' | 'pageview' | 'pageleave' + eventName: string + distinctId?: string + properties?: Record +} + +type ClientTelemetryListener = (event: ClientTelemetryEvent) => void + interface PostHogClientConfig { apiKey?: string apiHost?: string @@ -21,6 +32,7 @@ class PostHogClient { private pendingExposures: Array<{ experimentId: string; properties: Record }> = [] private config: PostHogClientConfig private readonly maxPendingEvents = MAX_PENDING_EVENTS + private devListeners: Set = new Set() constructor(config: PostHogClientConfig = {}) { const apiHost = @@ -119,6 +131,8 @@ class PostHogClient { } posthog.capture('$pageview', properties, { transport: 'sendBeacon' }) + + this.emitToDevListeners('pageview', '$pageview', properties) } catch (error) { console.error('PostHog pageview capture failed:', error) } @@ -140,6 +154,8 @@ class PostHogClient { try { // Use sendBeacon for page leave to survive tab close posthog.capture('$pageleave', properties, { transport: 'sendBeacon' }) + + this.emitToDevListeners('pageleave', '$pageleave', properties) } catch (error) { console.error('PostHog pageleave capture failed:', error) } @@ -156,6 +172,8 @@ class PostHogClient { try { posthog.identify(userId, properties) + + this.emitToDevListeners('identify', '$identify', { userId, ...properties }) } catch (error) { console.error('PostHog identify failed:', error) } @@ -285,6 +303,44 @@ class PostHogClient { console.error('PostHog experiment exposure capture failed:', error) } } + + subscribeToEvents(listener: ClientTelemetryListener): () => void { + this.devListeners.add(listener) + return () => this.devListeners.delete(listener) + } + + private emitToDevListeners( + eventType: ClientTelemetryEvent['eventType'], + eventName: string, + properties?: Record + ) { + if (this.devListeners.size === 0) return + + let distinctId: string | undefined + try { + const id = posthog.get_distinct_id?.() + if (id && id.length > 0) { + distinctId = id + } + } catch {} + + const event: ClientTelemetryEvent = { + id: `client-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + timestamp: Date.now(), + eventType, + eventName, + distinctId, + properties, + } + + this.devListeners.forEach((listener) => { + try { + listener(event) + } catch (e) { + console.error('Dev telemetry listener error:', e) + } + }) + } } export const posthogClient = new PostHogClient() diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index a1965ed9ff3..29c416c8bd9 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -14,7 +14,7 @@ import { useFeatureFlags } from './feature-flags' import { post } from './fetchWrappers' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useParams, useTelemetryCookie } from './hooks' -import { posthogClient } from './posthog-client' +import { posthogClient, type ClientTelemetryEvent } from './posthog-client' import { TelemetryEvent } from './telemetry-constants' import { clearTelemetryDataCookie, @@ -22,7 +22,7 @@ import { getTelemetryCookieOptions, } from './telemetry-utils' -export { posthogClient } +export { posthogClient, type ClientTelemetryEvent } const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS diff --git a/packages/dev-tools/DevToolbar.test.tsx b/packages/dev-tools/DevToolbar.test.tsx new file mode 100644 index 00000000000..0029db6dfd3 --- /dev/null +++ b/packages/dev-tools/DevToolbar.test.tsx @@ -0,0 +1,386 @@ +import { act, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +declare global { + interface Window { + devTelemetry?: () => void + } +} + +// Mock common package +vi.mock('common', async () => { + return { + useParams: () => ({ ref: 'default' }), + useFeatureFlags: () => ({ + posthog: {}, + configcat: {}, + }), + posthogClient: { + subscribeToEvents: vi.fn(() => () => {}), + }, + ensurePlatformSuffix: (url: string) => url, + } +}) + +const originalEnv = process.env.NODE_ENV + +/** + * Helper to render the full component tree as used in production. + * The Provider sets up window.devTelemetry and manages state. + * The Trigger shows the activity icon in the header. + * The Toolbar is the actual panel/sheet. + */ +async function renderFullToolbar() { + const { DevToolbarProvider } = await import('./DevToolbarContext') + const { DevToolbarTrigger } = await import('./DevToolbarTrigger') + const { DevToolbar } = await import('./DevToolbar') + + return render( + + + + + ) +} + +describe('DevToolbar', () => { + beforeEach(() => { + const store = new Map() + const localStorageMock = { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { + store.set(key, value) + }, + removeItem: (key: string) => { + store.delete(key) + }, + clear: () => { + store.clear() + }, + key: (index: number) => Array.from(store.keys())[index] ?? null, + get length() { + return store.size + }, + } + + Object.defineProperty(globalThis, 'localStorage', { + value: localStorageMock, + writable: true, + }) + + localStorage.clear() + delete window.devTelemetry + vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + process.env.NODE_ENV = originalEnv + vi.resetModules() + vi.restoreAllMocks() + }) + + describe('when not in local development', () => { + beforeEach(() => { + process.env.NODE_ENV = 'production' + }) + + it('returns null and does not render anything', async () => { + vi.resetModules() + const { container } = await renderFullToolbar() + + // Neither trigger nor toolbar should render in production + expect(container.querySelector('button')).toBeNull() + }) + + it('does not register window.devTelemetry', async () => { + vi.resetModules() + await renderFullToolbar() + + expect(window.devTelemetry).toBeUndefined() + }) + }) + + describe('when in local development but not enabled', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development' + }) + + it('does not render trigger when toolbar is not enabled', async () => { + vi.resetModules() + const { container } = await renderFullToolbar() + + // Trigger should not render when not enabled + expect(container.querySelector('button')).toBeNull() + }) + + it('registers window.devTelemetry function', async () => { + vi.resetModules() + await renderFullToolbar() + + expect(window.devTelemetry).toBeDefined() + expect(typeof window.devTelemetry).toBe('function') + }) + }) + + describe('when in local development and enabled', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development' + localStorage.setItem('dev-telemetry-toolbar-enabled', 'true') + }) + + it('renders the trigger button when enabled via localStorage', async () => { + vi.resetModules() + await renderFullToolbar() + + // The trigger should be a button with the Activity icon + const triggerButton = screen.getByRole('button') + expect(triggerButton).toBeInTheDocument() + }) + + it('opens the toolbar sheet when trigger is clicked', async () => { + vi.resetModules() + const user = userEvent.setup() + await renderFullToolbar() + + const triggerButton = screen.getByRole('button') + await user.click(triggerButton) + + // Sheet should open with the title + await waitFor(() => { + expect(screen.getByText('Dev Telemetry')).toBeInTheDocument() + }) + }) + + it('shows Events and Flags tabs in the toolbar', async () => { + vi.resetModules() + const user = userEvent.setup() + await renderFullToolbar() + + const triggerButton = screen.getByRole('button') + await user.click(triggerButton) + + await waitFor(() => { + expect(screen.getByRole('tab', { name: /Events/i })).toBeInTheDocument() + expect(screen.getByRole('tab', { name: /Flags/i })).toBeInTheDocument() + }) + }) + + it('shows "Local Only" badge in toolbar header', async () => { + vi.resetModules() + const user = userEvent.setup() + await renderFullToolbar() + + const triggerButton = screen.getByRole('button') + await user.click(triggerButton) + + await waitFor(() => { + expect(screen.getByText('Local Only')).toBeInTheDocument() + }) + }) + }) + + describe('window.devTelemetry function', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development' + }) + + it('enables toolbar when called', async () => { + vi.resetModules() + const { rerender } = await renderFullToolbar() + + // Trigger should not be visible initially + expect(screen.queryByRole('button')).not.toBeInTheDocument() + + // Call devTelemetry to enable + act(() => { + window.devTelemetry?.() + }) + + // Re-import and rerender to pick up state change + vi.resetModules() + const { DevToolbarProvider } = await import('./DevToolbarContext') + const { DevToolbarTrigger } = await import('./DevToolbarTrigger') + const { DevToolbar } = await import('./DevToolbar') + + rerender( + + + + + ) + + expect(localStorage.getItem('dev-telemetry-toolbar-enabled')).toBe('true') + }) + }) + + describe('cleanup', () => { + it('removes window.devTelemetry on unmount', async () => { + process.env.NODE_ENV = 'development' + + vi.resetModules() + const result = await renderFullToolbar() + + expect(window.devTelemetry).toBeDefined() + + result.unmount() + + expect(window.devTelemetry).toBeUndefined() + }) + }) + + describe('EventCard keyboard accessibility', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development' + localStorage.setItem('dev-telemetry-toolbar-enabled', 'true') + }) + + it('toolbar renders correctly with empty events state', async () => { + vi.resetModules() + + const user = userEvent.setup() + await renderFullToolbar() + + const triggerButton = screen.getByRole('button') + await user.click(triggerButton) + + await waitFor(() => { + expect(screen.getByText('Dev Telemetry')).toBeInTheDocument() + }) + + // Events tab should be active by default and show empty state + expect(screen.getByText(/No events yet/i)).toBeInTheDocument() + }) + }) + + describe('Flag override UI', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development' + localStorage.setItem('dev-telemetry-toolbar-enabled', 'true') + }) + + it('shows PostHog and ConfigCat sub-tabs in Flags tab', async () => { + vi.resetModules() + const user = userEvent.setup() + await renderFullToolbar() + + const triggerButton = screen.getByRole('button') + await user.click(triggerButton) + + // Switch to Flags tab + await waitFor(() => { + expect(screen.getByRole('tab', { name: /Flags/i })).toBeInTheDocument() + }) + + const flagsTab = screen.getByRole('tab', { name: /Flags/i }) + await user.click(flagsTab) + + await waitFor(() => { + expect(screen.getByRole('tab', { name: /PostHog/i })).toBeInTheDocument() + expect(screen.getByRole('tab', { name: /ConfigCat/i })).toBeInTheDocument() + }) + }) + }) +}) + +describe('DevToolbar utils', () => { + describe('safeJsonParse', () => { + it('logs warning for invalid JSON in local environment', async () => { + process.env.NODE_ENV = 'development' + const consoleSpy = vi.spyOn(console, 'warn') + + vi.resetModules() + const { safeJsonParse } = await import('./utils') + + const result = safeJsonParse('invalid json', {}, 'test context') + + expect(result).toEqual({}) + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('[DevToolbar] Failed to parse JSON'), + expect.anything() + ) + }) + + it('returns fallback for undefined input', async () => { + vi.resetModules() + const { safeJsonParse } = await import('./utils') + + const result = safeJsonParse(undefined, { default: true }) + + expect(result).toEqual({ default: true }) + }) + + it('parses valid JSON correctly', async () => { + vi.resetModules() + const { safeJsonParse } = await import('./utils') + + const result = safeJsonParse('{"key": "value"}', {}) + + expect(result).toEqual({ key: 'value' }) + }) + }) + + describe('parseOverrideValue', () => { + it('preserves number type when original is number', async () => { + vi.resetModules() + const { parseOverrideValue } = await import('./utils') + + // String input should be converted to number + expect(parseOverrideValue('42', 0)).toBe(42) + expect(parseOverrideValue('3.14', 0)).toBe(3.14) + }) + + it('returns original for invalid number strings', async () => { + vi.resetModules() + const { parseOverrideValue } = await import('./utils') + + expect(parseOverrideValue('not a number', 5)).toBe(5) + }) + + it('preserves boolean type', async () => { + vi.resetModules() + const { parseOverrideValue } = await import('./utils') + + expect(parseOverrideValue(true, false)).toBe(true) + expect(parseOverrideValue(false, true)).toBe(false) + }) + + it('preserves string type', async () => { + vi.resetModules() + const { parseOverrideValue } = await import('./utils') + + expect(parseOverrideValue('new value', 'original')).toBe('new value') + expect(parseOverrideValue(123, 'original')).toBe('123') + }) + }) + + describe('valuesAreEqual', () => { + it('handles number/string comparison', async () => { + vi.resetModules() + const { valuesAreEqual } = await import('./utils') + + expect(valuesAreEqual(42, '42')).toBe(true) + expect(valuesAreEqual('42', 42)).toBe(true) + expect(valuesAreEqual(42, '43')).toBe(false) + }) + + it('handles boolean comparison strictly', async () => { + vi.resetModules() + const { valuesAreEqual } = await import('./utils') + + expect(valuesAreEqual(true, true)).toBe(true) + expect(valuesAreEqual(false, false)).toBe(true) + expect(valuesAreEqual(true, false)).toBe(false) + }) + + it('handles null values', async () => { + vi.resetModules() + const { valuesAreEqual } = await import('./utils') + + expect(valuesAreEqual(null, null)).toBe(true) + expect(valuesAreEqual(null, 'value')).toBe(false) + expect(valuesAreEqual('value', null)).toBe(false) + }) + }) +}) diff --git a/packages/dev-tools/DevToolbar.tsx b/packages/dev-tools/DevToolbar.tsx new file mode 100644 index 00000000000..3fcc9eac762 --- /dev/null +++ b/packages/dev-tools/DevToolbar.tsx @@ -0,0 +1,438 @@ +'use client' + +import { useFeatureFlags } from 'common' +import { Activity, ChevronDown, ChevronUp, Flag } from 'lucide-react' +import { + type ChangeEvent, + type Dispatch, + type SetStateAction, + useCallback, + useEffect, + useState, +} from 'react' +import { + Badge, + Button, + Input_Shadcn_ as Input, + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + Switch, + Tabs_Shadcn_ as Tabs, + TabsContent_Shadcn_ as TabsContent, + TabsList_Shadcn_ as TabsList, + TabsTrigger_Shadcn_ as TabsTrigger, + cn, +} from 'ui' + +import { useDevToolbar } from './DevToolbarContext' +import type { DevTelemetryEvent } from './types' +import { + CC_ORIGINALS_KEY, + PH_ORIGINALS_KEY, + deleteCookie, + getCookie, + parseOverrideValue, + readOriginals, + safeJsonParse, + setCookie, + valuesAreEqual, + writeOriginals, +} from './utils' + +const IS_LOCAL_DEV = process.env.NODE_ENV === 'development' + +function EventCard({ event }: { event: DevTelemetryEvent }) { + const [isExpanded, setIsExpanded] = useState(false) + + const handleToggle = () => setIsExpanded((prev) => !prev) + + return ( +
+ + + {isExpanded && ( +
+          {JSON.stringify(event.properties, null, 2)}
+        
+ )} +
+ ) +} + +function FlagCard({ + flagName, + currentValue, + originalValue, + isOverridden, + onToggle, +}: { + flagName: string + currentValue: unknown + originalValue: unknown + isOverridden: boolean + onToggle: (value: unknown) => void +}) { + const valueType = typeof originalValue + const isNull = originalValue === null + const inputProps = { + value: String(currentValue), + onChange: (event: ChangeEvent) => onToggle(event.target.value), + className: 'w-32', + } + + return ( +
+
+
+ {flagName} + {isOverridden && ( + + Overridden + + )} + {isNull && ( + + null + + )} +
+ + {isNull ? ( + + ) : valueType === 'boolean' ? ( + onToggle(checked)} + /> + ) : valueType === 'number' ? ( + + ) : ( + + )} +
+ + {isOverridden && ( +
+ Original:{' '} + {JSON.stringify(originalValue)} +
+ )} +
+ ) +} + +export function DevToolbar() { + const { isEnabled, isOpen, setIsOpen, events, setEvents } = useDevToolbar() + const [activeTab, setActiveTab] = useState('events') + const [flagsSubTab, setFlagsSubTab] = useState<'posthog' | 'configcat'>('posthog') + const [eventFilter, setEventFilter] = useState('') + const { posthog: posthogFlags, configcat: configcatFlags } = useFeatureFlags() + const [phFlagOverrides, setPhFlagOverrides] = useState>({}) + const [ccFlagOverrides, setCcFlagOverrides] = useState>({}) + const [phFlagOriginals, setPhFlagOriginals] = useState>({}) + const [ccFlagOriginals, setCcFlagOriginals] = useState>({}) + + const loadOverrides = useCallback( + (cookieName: string, label: string, setter: (value: Record) => void) => { + const parsed = safeJsonParse>(getCookie(cookieName), {}, label) + if (Object.keys(parsed).length > 0) { + setter(parsed) + } + }, + [] + ) + + const saveOverrides = useCallback( + ( + cookieName: string, + overrides: Record, + setter: (value: Record) => void + ) => { + setter(overrides) + if (Object.keys(overrides).length > 0) { + setCookie(cookieName, JSON.stringify(overrides), '/') + } else { + deleteCookie(cookieName) + } + }, + [] + ) + + const updateOriginals = useCallback( + ( + storageKey: typeof PH_ORIGINALS_KEY | typeof CC_ORIGINALS_KEY, + setter: Dispatch>> + ) => + (updater: (prev: Record) => Record) => { + setter((prev) => { + const next = updater(prev) + writeOriginals(storageKey, next) + return next + }) + }, + [] + ) + + useEffect(() => { + loadOverrides('x-ph-flag-overrides', 'PostHog flag overrides', setPhFlagOverrides) + loadOverrides('x-cc-flag-overrides', 'ConfigCat flag overrides', setCcFlagOverrides) + }, [loadOverrides]) + + useEffect(() => { + setPhFlagOriginals(readOriginals(PH_ORIGINALS_KEY)) + setCcFlagOriginals(readOriginals(CC_ORIGINALS_KEY)) + }, []) + + const updatePhOriginals = updateOriginals(PH_ORIGINALS_KEY, setPhFlagOriginals) + const updateCcOriginals = updateOriginals(CC_ORIGINALS_KEY, setCcFlagOriginals) + + const togglePhFlagOverride = (flagName: string, value: unknown) => { + const originalValue = phFlagOriginals[flagName] ?? posthogFlags[flagName] + const parsedValue = parseOverrideValue(value, originalValue) + if (valuesAreEqual(parsedValue, originalValue)) { + const newOverrides = { ...phFlagOverrides } + delete newOverrides[flagName] + saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides) + return + } + + updatePhOriginals((prev) => + flagName in prev ? prev : { ...prev, [flagName]: posthogFlags[flagName] } + ) + const newOverrides = { ...phFlagOverrides, [flagName]: parsedValue } + saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides) + } + + const toggleCcFlagOverride = (flagName: string, value: unknown) => { + const originalValue = ccFlagOriginals[flagName] ?? configcatFlags[flagName] + const parsedValue = parseOverrideValue(value, originalValue) + if (valuesAreEqual(parsedValue, originalValue)) { + const newOverrides = { ...ccFlagOverrides } + delete newOverrides[flagName] + saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides) + return + } + + updateCcOriginals((prev) => + flagName in prev ? prev : { ...prev, [flagName]: configcatFlags[flagName] } + ) + const newOverrides = { ...ccFlagOverrides, [flagName]: parsedValue } + saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides) + } + + const clearAllOverrides = () => { + setPhFlagOverrides({}) + setCcFlagOverrides({}) + setPhFlagOriginals({}) + setCcFlagOriginals({}) + deleteCookie('x-ph-flag-overrides') + deleteCookie('x-cc-flag-overrides') + writeOriginals(PH_ORIGINALS_KEY, {}) + writeOriginals(CC_ORIGINALS_KEY, {}) + window.location.reload() + } + + const normalizedFilter = eventFilter.trim().toLowerCase() + const filteredEvents = ( + normalizedFilter + ? events.filter( + (e) => + e.eventName.toLowerCase().includes(normalizedFilter) || + e.eventType.toLowerCase().includes(normalizedFilter) + ) + : events + ) + .slice() + .sort((a, b) => b.timestamp - a.timestamp) + + const phOverrideCount = Object.keys(phFlagOverrides).length + const ccOverrideCount = Object.keys(ccFlagOverrides).length + const totalOverrideCount = phOverrideCount + ccOverrideCount + + if (!IS_LOCAL_DEV || !isEnabled) return null + + return ( + + event.preventDefault()} + > + +
+ + Dev Telemetry + Local Only +
+ + View telemetry events and feature flags for local development + +
+ +
+ + + + + Events ({filteredEvents.length}) + + + + Flags {totalOverrideCount > 0 && `(${totalOverrideCount} overrides)`} + + + {activeTab === 'events' && ( +
+
+ setEventFilter(e.target.value)} + className="flex-1" + /> + +
+ +
+ {filteredEvents.length === 0 ? ( +
+ No events yet. Interact with the app to see telemetry events. +
+ ) : ( + filteredEvents.map((event) => ( + + )) + )} +
+
+ )} + + {activeTab === 'flags' && ( +
+ {totalOverrideCount > 0 && ( +
+ + {totalOverrideCount} flag(s) overridden + {phOverrideCount > 0 && ccOverrideCount > 0 + ? ` (${phOverrideCount} PostHog, ${ccOverrideCount} ConfigCat)` + : ''} + + +
+ )} + + setFlagsSubTab(v as 'posthog' | 'configcat')} + className="flex-1 flex flex-col min-h-0 overflow-hidden" + > + + + PostHog {phOverrideCount > 0 && `(${phOverrideCount})`} + + + ConfigCat {ccOverrideCount > 0 && `(${ccOverrideCount})`} + + + + {flagsSubTab === 'posthog' && ( +
+
+ {Object.keys(posthogFlags).length === 0 ? ( +
+ No PostHog feature flags loaded yet. +
+ ) : ( + Object.entries(posthogFlags).map(([flagName, flagValue]) => ( + togglePhFlagOverride(flagName, value)} + /> + )) + )} +
+
+ )} + {flagsSubTab === 'configcat' && ( +
+
+ {Object.keys(configcatFlags).length === 0 ? ( +
+ No ConfigCat feature flags loaded yet. +
+ ) : ( + Object.entries(configcatFlags).map(([flagName, flagValue]) => ( + toggleCcFlagOverride(flagName, value)} + /> + )) + )} +
+
+ )} +
+
+ )} +
+
+
+
+ ) +} diff --git a/packages/dev-tools/DevToolbarContext.tsx b/packages/dev-tools/DevToolbarContext.tsx new file mode 100644 index 00000000000..e1ef961fd97 --- /dev/null +++ b/packages/dev-tools/DevToolbarContext.tsx @@ -0,0 +1,212 @@ +'use client' + +import { type ClientTelemetryEvent, ensurePlatformSuffix, posthogClient } from 'common' +import { + type ReactNode, + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react' + +import type { + DevTelemetryEvent, + DevTelemetryToolbarContextType, + ServerTelemetryEvent, +} from './types' +import { getCookie } from './utils' + +const IS_LOCAL_DEV = process.env.NODE_ENV === 'development' +const MAX_EVENTS = 200 +const STORAGE_KEY = 'dev-telemetry-toolbar-enabled' + +const SSE_INITIAL_RETRY_MS = 1000 +const SSE_MAX_RETRY_MS = 30000 +const SSE_BACKOFF_MULTIPLIER = 2 + +declare global { + interface Window { + devTelemetry?: () => void + } +} + +const DevToolbarContext = createContext(null) + +interface DevToolbarProviderProps { + children: ReactNode + apiUrl: string +} + +export function DevToolbarProvider({ children, apiUrl }: DevToolbarProviderProps) { + const [isEnabled, setIsEnabled] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const [events, setEvents] = useState([]) + + const sseRetryDelayRef = useRef(SSE_INITIAL_RETRY_MS) + const sseRetryTimeoutRef = useRef(null) + + const dismissToolbar = useCallback(() => { + try { + localStorage.removeItem(STORAGE_KEY) + } catch {} + setIsEnabled(false) + setIsOpen(false) + }, []) + + useEffect(() => { + if (!IS_LOCAL_DEV) return + + let stored: string | null = null + try { + stored = localStorage.getItem(STORAGE_KEY) + } catch {} + if (stored === 'true') { + setIsEnabled(true) + } + + window.devTelemetry = () => { + try { + localStorage.setItem(STORAGE_KEY, 'true') + } catch {} + setIsEnabled(true) + } + + return () => { + delete window.devTelemetry + } + }, []) + + const appendEvent = useCallback((event: DevTelemetryEvent) => { + setEvents((prev) => { + const key = `${event.source}-${event.id}` + if (prev.some((e) => `${e.source}-${e.id}` === key)) return prev + return [...prev.slice(-(MAX_EVENTS - 1)), event] + }) + }, []) + + useEffect(() => { + if (!isEnabled) return + + const unsubscribe = posthogClient.subscribeToEvents((clientEvent: ClientTelemetryEvent) => { + appendEvent({ + id: clientEvent.id, + timestamp: clientEvent.timestamp, + source: 'client', + eventType: clientEvent.eventType, + eventName: clientEvent.eventName, + distinctId: clientEvent.distinctId, + properties: clientEvent.properties, + }) + }) + + return unsubscribe + }, [appendEvent, isEnabled]) + + useEffect(() => { + if (!isEnabled || typeof EventSource === 'undefined') return + + let eventSource: EventSource | null = null + let isMounted = true + + const connect = () => { + if (!isMounted) return + + const sessionId = getCookie('session_id') + const streamUrl = `${ensurePlatformSuffix(apiUrl)}/telemetry/stream${ + sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : '' + }` + + eventSource = new EventSource(streamUrl, { withCredentials: true }) + + eventSource.onopen = () => { + sseRetryDelayRef.current = SSE_INITIAL_RETRY_MS + } + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as ServerTelemetryEvent + appendEvent({ + id: data.id, + timestamp: data.timestamp, + source: 'server', + eventType: data.eventType, + eventName: data.eventName, + distinctId: data.distinctId, + properties: data.properties, + }) + } catch (e) { + console.error('[DevToolbar] Failed to parse SSE event:', e) + } + } + + eventSource.onerror = () => { + if (!isMounted) return + + eventSource?.close() + eventSource = null + + const delay = sseRetryDelayRef.current + console.warn(`[DevToolbar] SSE connection error, reconnecting in ${delay}ms...`) + + if (sseRetryTimeoutRef.current) { + clearTimeout(sseRetryTimeoutRef.current) + sseRetryTimeoutRef.current = null + } + sseRetryTimeoutRef.current = setTimeout(() => { + if (isMounted) { + connect() + } + }, delay) + + sseRetryDelayRef.current = Math.min(delay * SSE_BACKOFF_MULTIPLIER, SSE_MAX_RETRY_MS) + } + } + + connect() + + return () => { + isMounted = false + eventSource?.close() + if (sseRetryTimeoutRef.current) { + clearTimeout(sseRetryTimeoutRef.current) + sseRetryTimeoutRef.current = null + } + } + }, [apiUrl, appendEvent, isEnabled]) + + if (!IS_LOCAL_DEV) { + return <>{children} + } + + return ( + + {children} + + ) +} + +export function useDevToolbar() { + const context = useContext(DevToolbarContext) + if (!context) { + return { + isEnabled: false, + isOpen: false, + setIsOpen: () => {}, + events: [], + setEvents: () => {}, + dismissToolbar: () => {}, + } + } + return context +} diff --git a/packages/dev-tools/DevToolbarTrigger.tsx b/packages/dev-tools/DevToolbarTrigger.tsx new file mode 100644 index 00000000000..1a64eb43348 --- /dev/null +++ b/packages/dev-tools/DevToolbarTrigger.tsx @@ -0,0 +1,113 @@ +'use client' + +import { Activity, EyeOff } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import type { MouseEvent } from 'react' +import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui' + +import { useDevToolbar } from './DevToolbarContext' + +const IS_LOCAL_DEV = process.env.NODE_ENV === 'development' + +export function DevToolbarTrigger() { + const { isEnabled, isOpen, setIsOpen, events, dismissToolbar } = useDevToolbar() + const [popoverOpen, setPopoverOpen] = useState(false) + const closeTimeoutRef = useRef(null) + + useEffect(() => { + return () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current) + closeTimeoutRef.current = null + } + } + }, []) + + if (!IS_LOCAL_DEV || !isEnabled) { + return null + } + + const eventCount = events.length + + const handleClick = () => { + setPopoverOpen(false) + setIsOpen(true) + } + + const handleDismiss = () => { + setPopoverOpen(false) + dismissToolbar() + } + + const handleMouseEnter = () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current) + closeTimeoutRef.current = null + } + setPopoverOpen(true) + } + + const handleMouseLeave = () => { + closeTimeoutRef.current = setTimeout(() => { + setPopoverOpen(false) + }, 100) + } + + const handleTriggerMouseLeave = (event: MouseEvent) => { + handleMouseLeave() + event.currentTarget.blur() + } + + return ( + + + + + event.preventDefault()} + > + + + + ) +} diff --git a/packages/dev-tools/index.ts b/packages/dev-tools/index.ts new file mode 100644 index 00000000000..e682df49fa3 --- /dev/null +++ b/packages/dev-tools/index.ts @@ -0,0 +1,36 @@ +import type { ReactNode } from 'react' + +import * as DevToolbarModule from './DevToolbar' +import * as DevToolbarContextModule from './DevToolbarContext' +import * as DevToolbarTriggerModule from './DevToolbarTrigger' +import type { DevTelemetryToolbarContextType } from './types' + +// Tree-shaking pattern: conditionally export stubs in production +// The bundler replaces process.env.NODE_ENV at build time, making the +// ternary static. Combined with sideEffects: false, the implementation +// modules are eliminated from the production bundle. + +const noopContext: DevTelemetryToolbarContextType = { + isEnabled: false, + isOpen: false, + setIsOpen: () => {}, + events: [], + setEvents: () => {}, + dismissToolbar: () => {}, +} + +export const DevToolbarProvider = + process.env.NODE_ENV !== 'development' + ? ({ children }: { children: ReactNode; apiUrl?: string }) => children + : DevToolbarContextModule.DevToolbarProvider + +export const useDevToolbar = + process.env.NODE_ENV !== 'development' ? () => noopContext : DevToolbarContextModule.useDevToolbar + +export const DevToolbar = + process.env.NODE_ENV !== 'development' ? () => null : DevToolbarModule.DevToolbar + +export const DevToolbarTrigger = + process.env.NODE_ENV !== 'development' ? () => null : DevToolbarTriggerModule.DevToolbarTrigger + +export type { DevTelemetryEvent, DevToolbarConfig } from './types' diff --git a/packages/dev-tools/package.json b/packages/dev-tools/package.json new file mode 100644 index 00000000000..251ff9b1282 --- /dev/null +++ b/packages/dev-tools/package.json @@ -0,0 +1,39 @@ +{ + "name": "dev-tools", + "version": "0.0.0", + "private": true, + "sideEffects": false, + "type": "module", + "main": "./index.ts", + "types": "./index.ts", + "exports": { + ".": "./index.ts" + }, + "scripts": { + "preinstall": "npx only-allow pnpm", + "clean": "rimraf node_modules", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "common": "workspace:*", + "lucide-react": "*", + "ui": "workspace:*" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.0.0", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "next-router-mock": "^0.9.13", + "tsconfig": "workspace:*", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "peerDependencies": { + "react": "catalog:", + "react-dom": "catalog:" + } +} diff --git a/packages/dev-tools/tsconfig.json b/packages/dev-tools/tsconfig.json new file mode 100644 index 00000000000..caf116d7d76 --- /dev/null +++ b/packages/dev-tools/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "outDir": "dist", + "resolveJsonModule": true + }, + "include": [".", "../ui/src/**/*.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/dev-tools/types.ts b/packages/dev-tools/types.ts new file mode 100644 index 00000000000..a55dffe3933 --- /dev/null +++ b/packages/dev-tools/types.ts @@ -0,0 +1,35 @@ +import type { Dispatch, SetStateAction } from 'react' + +export interface DevTelemetryEvent { + id: string + timestamp: number + source: 'client' | 'server' + eventType: string + eventName: string + distinctId?: string + properties?: Record +} + +export interface ServerTelemetryEvent { + id: string + timestamp: number + sessionId: string + eventType: 'capture' | 'identify' | 'groupIdentify' | 'alias' + eventName: string + distinctId: string + properties?: Record + groups?: Record +} + +export interface DevToolbarConfig { + apiUrl: string +} + +export interface DevTelemetryToolbarContextType { + isEnabled: boolean + isOpen: boolean + setIsOpen: (open: boolean) => void + events: DevTelemetryEvent[] + setEvents: Dispatch> + dismissToolbar: () => void +} diff --git a/packages/dev-tools/utils.ts b/packages/dev-tools/utils.ts new file mode 100644 index 00000000000..c8101330f47 --- /dev/null +++ b/packages/dev-tools/utils.ts @@ -0,0 +1,90 @@ +export function getCookie(name: string): string | undefined { + if (typeof document === 'undefined') return undefined + const escapedName = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + const match = document.cookie.match(new RegExp(`(^| )${escapedName}=([^;]+)`)) + return match ? decodeURIComponent(match[2]) : undefined +} + +export function setCookie(name: string, value: string, path: string = '/') { + if (typeof document === 'undefined') return + document.cookie = `${name}=${encodeURIComponent(value)}; path=${path}` +} + +export function deleteCookie(name: string) { + if (typeof document === 'undefined') return + document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT` +} + +export function safeJsonParse(value: string | undefined, fallback: T, context?: string): T { + if (!value) return fallback + try { + return JSON.parse(value) as T + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn(`[DevToolbar] Failed to parse JSON${context ? ` for ${context}` : ''}:`, error) + } + return fallback + } +} + +export const PH_ORIGINALS_KEY = 'devToolbarFlagOriginals:posthog' +export const CC_ORIGINALS_KEY = 'devToolbarFlagOriginals:configcat' + +export function readOriginals( + key: typeof PH_ORIGINALS_KEY | typeof CC_ORIGINALS_KEY +): Record { + if (typeof window === 'undefined') return {} + try { + const stored = window.localStorage.getItem(key) + return stored ? JSON.parse(stored) : {} + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn(`[DevToolbar] Failed to read originals from ${key}:`, error) + } + return {} + } +} + +export function writeOriginals( + key: typeof PH_ORIGINALS_KEY | typeof CC_ORIGINALS_KEY, + value: Record +) { + if (typeof window === 'undefined') return + if (Object.keys(value).length === 0) { + window.localStorage.removeItem(key) + return + } + window.localStorage.setItem(key, JSON.stringify(value)) +} + +export function valuesAreEqual(a: unknown, b: unknown): boolean { + if (a === b) return true + if (a == null || b == null) return false + if (typeof a === 'number' || typeof b === 'number') { + const numA = Number(a) + const numB = Number(b) + if (Number.isNaN(numA) || Number.isNaN(numB)) return false + return numA === numB + } + if (typeof a === 'boolean' || typeof b === 'boolean') { + return a === b + } + return String(a) === String(b) +} + +export function parseOverrideValue(value: unknown, original: unknown): unknown { + if (typeof original === 'number') { + const parsed = Number(value) + return Number.isNaN(parsed) ? original : parsed + } + if (typeof original === 'boolean') { + if (typeof value === 'string') { + return value.toLowerCase() === 'true' + } + return Boolean(value) + } + if (typeof original === 'string') { + return String(value) + } + return value +} diff --git a/packages/dev-tools/vitest.config.ts b/packages/dev-tools/vitest.config.ts new file mode 100644 index 00000000000..489cb49b282 --- /dev/null +++ b/packages/dev-tools/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + reporters: [['default']], + coverage: { + reporter: ['lcov'], + exclude: ['**/*.test.ts', '**/*.test.tsx'], + include: ['**/*.ts', '**/*.tsx'], + }, + }, +}) diff --git a/packages/dev-tools/vitest.setup.ts b/packages/dev-tools/vitest.setup.ts new file mode 100644 index 00000000000..3d8e21849f6 --- /dev/null +++ b/packages/dev-tools/vitest.setup.ts @@ -0,0 +1,25 @@ +/// + +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterEach, vi } from 'vitest' + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) + +vi.mock('next/navigation', () => require('next-router-mock/navigation')) + +afterEach(() => { + cleanup() +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faa4dd2006c..73db6c42f31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -468,6 +468,9 @@ importers: config: specifier: workspace:* version: link:../../packages/config + dev-tools: + specifier: workspace:* + version: link:../../packages/dev-tools eslint-config-supabase: specifier: workspace:* version: link:../../packages/eslint-config-supabase @@ -1238,6 +1241,9 @@ importers: dayjs: specifier: ^1.11.10 version: 1.11.13 + dev-tools: + specifier: workspace:* + version: link:../../packages/dev-tools dnd-core: specifier: ^16.0.1 version: 16.0.1 @@ -1960,6 +1966,9 @@ importers: dayjs: specifier: ^1.11.12 version: 1.11.13 + dev-tools: + specifier: workspace:* + version: link:../../packages/dev-tools eslint-config-supabase: specifier: workspace:* version: link:../../packages/eslint-config-supabase @@ -2444,6 +2453,52 @@ importers: specifier: 'catalog:' version: 5.9.2 + packages/dev-tools: + dependencies: + common: + specifier: workspace:* + version: link:../common + lucide-react: + specifier: '*' + version: 0.511.0(react@18.3.1) + react: + specifier: 'catalog:' + version: 18.3.1 + react-dom: + specifier: 'catalog:' + version: 18.3.1(react@18.3.1) + ui: + specifier: workspace:* + version: link:../ui + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.6.0 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.0.0 + version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.0.0 + version: 14.6.1(@testing-library/dom@10.4.0) + '@types/react': + specifier: 'catalog:' + version: 18.3.3 + '@types/react-dom': + specifier: 'catalog:' + version: 18.3.0 + next-router-mock: + specifier: ^0.9.13 + version: 0.9.13(next@16.0.11(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: 'catalog:' + version: 5.9.2 + vitest: + specifier: 'catalog:' + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + packages/eslint-config-supabase: devDependencies: '@eslint/eslintrc': @@ -28639,6 +28694,10 @@ snapshots: dependencies: '@testing-library/dom': 10.1.0 + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + '@tokenizer/token@0.3.0': {} '@tootallnate/once@1.1.2': {} From 997203cd64cef41fde13b8a53f619f7007008913 Mon Sep 17 00:00:00 2001 From: Kalleby Santos <105971119+kallebysantos@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:06:37 +0000 Subject: [PATCH 05/22] feat(studio-local): functions management api - function blob artifacts (#42349) ## What kind of change does this PR introduce? Feature ## What is the current behavior? Functions page on self-hosted differs from Platform ## What is the new behavior? > [!NOTE] > This PR only add readonly operations. Function edit and deploy should be implemented in a future one. Adds the possibility to download and see function code in Self-Host version.
image
## Summary by CodeRabbit * **New Features** * API endpoint to stream function files/artifacts as multipart/form-data. * New function file entry type and server-side file listing for functions. * **Improvements** * Edge Functions "Code" navigation item always visible. * Download popover reworked: ZIP download always available; CLI section shown only on supported platforms. * Editor set to read-only and file actions disabled on unsupported environments. * **Editor** * Added JavaScript, TypeScript, and Markdown language modules for the embedded editor. --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .../EdgeFunctionDetailsLayout.tsx | 54 +- .../ui/FileExplorerAndEditor/index.tsx | 74 +- .../edge-function-body-query.ts | 3 +- .../self-hosted/functions/fileSystemStore.ts | 30 +- .../lib/api/self-hosted/functions/types.ts | 8 + .../projects/[ref]/functions/[slug]/body.ts | 81 + .../[ref]/functions/[functionSlug]/code.tsx | 58 +- .../basic-languages/javascript/javascript.js | 10 + .../basic-languages/markdown/markdown.js | 10 + .../basic-languages/typescript/typescript.js | 10 + .../language/typescript/tsMode.js | 20 + .../language/typescript/tsWorker.js | 51328 ++++++++++++++++ 12 files changed, 51596 insertions(+), 90 deletions(-) create mode 100644 apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts create mode 100644 apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js create mode 100644 apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js create mode 100644 apps/studio/public/monaco-editor/basic-languages/typescript/typescript.js create mode 100644 apps/studio/public/monaco-editor/language/typescript/tsMode.js create mode 100644 apps/studio/public/monaco-editor/language/typescript/tsWorker.js diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index e78f9cefaad..895917be4f9 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -119,16 +119,16 @@ const EdgeFunctionDetailsLayout = ({ label: 'Logs', href: `/project/${ref}/functions/${functionSlug}/logs`, }, - { - label: 'Code', - href: `/project/${ref}/functions/${functionSlug}/code`, - }, ] : []), { label: 'Details', href: `/project/${ref}/functions/${functionSlug}/details`, }, + { + label: 'Code', + href: `/project/${ref}/functions/${functionSlug}/code`, + }, ] : [] @@ -264,15 +264,15 @@ const EdgeFunctionDetailsLayout = ({ /> )} - {IS_PLATFORM && ( - <> - - - - - + + + + + + {IS_PLATFORM && ( + <>

Download via CLI

-
- -
-
-
+ + )} +
+ +
+
+
+ {IS_PLATFORM && ( + <> {!!functionSlug && ( + {IS_PLATFORM && ( + + )}
} isEditing={isEditing} onEditSubmit={(value) => { - if (originalId !== null) { + if (IS_PLATFORM && originalId !== null) { handleFileNameChange(originalId, value) } }} @@ -424,7 +427,7 @@ export const FileExplorerAndEditor = ({ } }} onDoubleClick={() => { - if (originalId !== null) { + if (IS_PLATFORM && originalId !== null) { handleStartRename(originalId) } }} @@ -445,36 +448,38 @@ export const FileExplorerAndEditor = ({ />
- e.stopPropagation()}> - { - if (originalId !== null) handleStartRename(originalId) - }} - onFocusCapture={(e) => e.stopPropagation()} - > - - Rename file - + {IS_PLATFORM && ( + e.stopPropagation()}> + { + if (originalId !== null) handleStartRename(originalId) + }} + onFocusCapture={(e) => e.stopPropagation()} + > + + Rename file + - {files.length > 1 && ( - <> - - { - if (originalId !== null) { - handleFileDelete(originalId) - } - }} - onFocusCapture={(e) => e.stopPropagation()} - > - - Delete file - - - )} - + {files.length > 1 && ( + <> + + { + if (originalId !== null) { + handleFileDelete(originalId) + } + }} + onFocusCapture={(e) => e.stopPropagation()} + > + + Delete file + + + )} + + )} ) }} @@ -509,6 +514,7 @@ export const FileExplorerAndEditor = ({ padding: { top: 20, bottom: 20 }, lineNumbersMinChars: 3, fixedOverflowWidgets: true, + readOnly: !IS_PLATFORM, }} /> )} diff --git a/apps/studio/data/edge-functions/edge-function-body-query.ts b/apps/studio/data/edge-functions/edge-function-body-query.ts index 772a91ca6f2..8ee38e9a930 100644 --- a/apps/studio/data/edge-functions/edge-function-body-query.ts +++ b/apps/studio/data/edge-functions/edge-function-body-query.ts @@ -72,7 +72,6 @@ export const useEdgeFunctionBodyQuery = ( useQuery({ queryKey: edgeFunctionsKeys.body(projectRef, slug), queryFn: ({ signal }) => getEdgeFunctionBody({ projectRef, slug }, signal), - enabled: - IS_PLATFORM && enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined', ...options, }) diff --git a/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts b/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts index 3cd98f49239..f5b783866a8 100644 --- a/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts +++ b/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts @@ -3,7 +3,7 @@ import { readdir, stat } from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' -import { FunctionArtifact } from './types' +import { FunctionArtifact, FunctionFileEntry } from './types' export class FileSystemFunctionsArtifactStore { constructor(private folderPath: string) {} @@ -29,6 +29,34 @@ export class FileSystemFunctionsArtifactStore { return parseFolderToFunctionArtifact(functionFolder) } + + async getFileEntriesBySlug(slug: string): Promise> { + if (slug === 'main') return [] + + const functionFolderPath = path.resolve(this.folderPath, slug) + if (!functionFolderPath.startsWith(path.resolve(this.folderPath) + path.sep)) return [] + + const entries = await readdir(functionFolderPath, { + recursive: true, + withFileTypes: true, + }) + + const fileEntries = await Promise.all( + entries + .filter((entry) => entry.isFile()) + .map(async (entry) => { + const absolutePath = path.join(entry.parentPath, entry.name) + const fileStat = await stat(absolutePath) + return { + absolutePath, + relativePath: path.relative(functionFolderPath, absolutePath), + size: fileStat.size, + } + }) + ) + + return fileEntries + } } async function parseFolderToFunctionArtifact( diff --git a/apps/studio/lib/api/self-hosted/functions/types.ts b/apps/studio/lib/api/self-hosted/functions/types.ts index 0a124c69669..31f22cc79bc 100644 --- a/apps/studio/lib/api/self-hosted/functions/types.ts +++ b/apps/studio/lib/api/self-hosted/functions/types.ts @@ -4,3 +4,11 @@ export type FunctionArtifact = { created_at: number updated_at: number } + +export type FunctionFileEntry = { + /** Absolute path on disk */ + absolutePath: string + /** Path relative to the function folder, used as the multipart filename */ + relativePath: string + size: number +} diff --git a/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts b/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts new file mode 100644 index 00000000000..0ffb91ed316 --- /dev/null +++ b/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts @@ -0,0 +1,81 @@ +import { createReadStream } from 'node:fs' +import { pipeline } from 'node:stream/promises' +import { type NextApiRequest, type NextApiResponse } from 'next' + +import apiWrapper from '@/lib/api/apiWrapper' +import { getFunctionsArtifactStore } from '@/lib/api/self-hosted/functions' +import { uuidv4 } from '@/lib/helpers' + +export default function handlerWithErrorCatching(req: NextApiRequest, res: NextApiResponse) { + return apiWrapper(req, res, handler, { withAuth: true }) +} + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + default: + res.setHeader('Allow', ['GET']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +async function handleGet(req: NextApiRequest, res: NextApiResponse) { + const slugParam = req.query.slug + const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam + if (!slug) { + res.status(404).json({ error: { message: `Missing function 'slug' parameter` } }) + return + } + + const store = getFunctionsArtifactStore() + const fileEntries = await store.getFileEntriesBySlug(slug) + + const boundary = `----FormBoundary${uuidv4().replace(/-/g, '')}` + const totalSize = fileEntries.reduce((sum, entry) => sum + entry.size, 0) + + const metadata = { + // mock id, should be "__" + deployment_id: uuidv4(), + original_size: totalSize, + compressed_size: totalSize, + module_count: fileEntries.length, + } + + res.setHeader('Content-Type', `multipart/form-data; boundary=${boundary}`) + res.status(200) + + // Write metadata part + const metadataJson = JSON.stringify(metadata) + res.write( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="metadata"\r\n` + + `Content-Type: application/json\r\n` + + `\r\n` + + metadataJson + + `\r\n` + ) + + // Stream each file part + for (const entry of fileEntries) { + const safeName = entry.relativePath + .replace(/[\r\n]/g, '') + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + const encodedName = encodeURIComponent(entry.relativePath) + res.write( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="${safeName}"; filename*=UTF-8''${encodedName}\r\n` + + `Content-Type: text/plain\r\n` + + `\r\n` + ) + await pipeline(createReadStream(entry.absolutePath), res, { end: false }) + res.write(`\r\n`) + } + + // Write closing boundary + res.write(`--${boundary}--\r\n`) + res.end() +} diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx index d0026fc5c0c..76cc03bb879 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils' import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types' import { useLatest } from '@/hooks/misc/useLatest' -import { useParams } from 'common' +import { IS_PLATFORM, useParams } from 'common' import { DeployEdgeFunctionWarningModal } from 'components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal' import { DefaultLayout } from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' @@ -214,33 +214,35 @@ const CodePage = () => { orgSlug: org?.slug, }} /> -
- - ) : ( -
- -
- ) - } - tooltip={{ - content: { - side: 'top', - text: !canDeployFunction - ? 'You need additional permissions to update edge functions' - : undefined, - }, - }} - > - Deploy updates -
-
+ {IS_PLATFORM && ( +
+ + ) : ( +
+ +
+ ) + } + tooltip={{ + content: { + side: 'top', + text: !canDeployFunction + ? 'You need additional permissions to update edge functions' + : undefined, + }, + }} + > + Deploy updates +
+
+ )} )} diff --git a/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js b/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js new file mode 100644 index 00000000000..28682989697 --- /dev/null +++ b/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js @@ -0,0 +1,10 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1) + * Released under the MIT license + * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/javascript/javascript", ["require","require"],(require)=>{ +"use strict";var moduleExports=(()=>{var x=Object.create;var a=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var b=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var y=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var w=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),h=(e,t)=>{for(var n in t)a(e,n,{get:t[n],enumerable:!0})},s=(e,t,n,c)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of f(t))!k.call(e,r)&&r!==n&&a(e,r,{get:()=>t[r],enumerable:!(c=u(t,r))||c.enumerable});return e},g=(e,t,n)=>(s(e,t,"default"),n&&s(n,t,"default")),p=(e,t,n)=>(n=e!=null?x(b(e)):{},s(t||!e||!e.__esModule?a(n,"default",{value:e,enumerable:!0}):n,e)),v=e=>s(a({},"__esModule",{value:!0}),e);var d=w((C,l)=>{var A=p(y("vs/editor/editor.api"));l.exports=A});var _={};h(_,{conf:()=>$,language:()=>T});var i={};g(i,p(d()));var m={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],onEnterRules:[{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,afterText:/^\s*\*\/$/,action:{indentAction:i.languages.IndentAction.IndentOutdent,appendText:" * "}},{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,action:{indentAction:i.languages.IndentAction.None,appendText:" * "}},{beforeText:/^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,action:{indentAction:i.languages.IndentAction.None,appendText:"* "}},{beforeText:/^(\t|(\ \ ))*\ \*\/\s*$/,action:{indentAction:i.languages.IndentAction.None,removeText:1}}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],folding:{markers:{start:new RegExp("^\\s*//\\s*#?region\\b"),end:new RegExp("^\\s*//\\s*#?endregion\\b")}}},o={defaultToken:"invalid",tokenPostfix:".ts",keywords:["abstract","any","as","asserts","bigint","boolean","break","case","catch","class","continue","const","constructor","debugger","declare","default","delete","do","else","enum","export","extends","false","finally","for","from","function","get","if","implements","import","in","infer","instanceof","interface","is","keyof","let","module","namespace","never","new","null","number","object","out","package","private","protected","public","override","readonly","require","global","return","satisfies","set","static","string","super","switch","symbol","this","throw","true","try","type","typeof","undefined","unique","unknown","var","void","while","with","yield","async","await","of"],operators:["<=",">=","==","!=","===","!==","=>","+","-","**","*","/","%","++","--","<<",">",">>>","&","|","^","!","~","&&","||","??","?",":","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=","@"],symbols:/[=>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"],[/`/,"string","@string_backtick"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([dgimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],string_single:[[/[^\\']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"]],string_backtick:[[/\$\{/,{token:"delimiter.bracket",next:"@bracketCounting"}],[/[^\\`$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/`/,"string","@pop"]],bracketCounting:[[/\{/,"delimiter.bracket","@bracketCounting"],[/\}/,"delimiter.bracket","@pop"],{include:"common"}]}};var $=m,T={defaultToken:"invalid",tokenPostfix:".js",keywords:["break","case","catch","class","continue","const","constructor","debugger","default","delete","do","else","export","extends","false","finally","for","from","function","get","if","import","in","instanceof","let","new","null","return","set","static","super","switch","symbol","this","throw","true","try","typeof","undefined","var","void","while","with","yield","async","await","of"],typeKeywords:[],operators:o.operators,symbols:o.symbols,escapes:o.escapes,digits:o.digits,octaldigits:o.octaldigits,binarydigits:o.binarydigits,hexdigits:o.hexdigits,regexpctl:o.regexpctl,regexpesc:o.regexpesc,tokenizer:o.tokenizer};return v(_);})(); +return moduleExports; +}); diff --git a/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js b/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js new file mode 100644 index 00000000000..b9edf510978 --- /dev/null +++ b/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js @@ -0,0 +1,10 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1) + * Released under the MIT license + * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/markdown/markdown", ["require","require"],(require)=>{ +"use strict";var moduleExports=(()=>{var s=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var i=Object.prototype.hasOwnProperty;var l=(t,e)=>{for(var o in e)s(t,o,{get:e[o],enumerable:!0})},m=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of c(e))!i.call(t,n)&&n!==o&&s(t,n,{get:()=>e[n],enumerable:!(a=r(e,n))||a.enumerable});return t};var d=t=>m(s({},"__esModule",{value:!0}),t);var b={};l(b,{conf:()=>p,language:()=>g});var p={comments:{blockComment:[""]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*"),end:new RegExp("^\\s*")}}},g={defaultToken:"",tokenPostfix:".md",control:/[\\`*_\[\]{}()#+\-\.!]/,noncontrol:/[^\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,jsescapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],tokenizer:{root:[[/^\s*\|/,"@rematch","@table_header"],[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/,["white","keyword","keyword","keyword"]],[/^\s*(=+|\-+)\s*$/,"keyword"],[/^\s*((\*[ ]?)+)\s*$/,"meta.separator"],[/^\s*>+/,"comment"],[/^\s*([\*\-+:]|\d+\.)\s/,"keyword"],[/^(\t|[ ]{4})[^ ].*$/,"string"],[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,{token:"string",next:"@codeblock"}],[/^\s*```\s*((?:\w|[\/\-#])+).*$/,{token:"string",next:"@codeblockgh",nextEmbedded:"$1"}],[/^\s*```\s*$/,{token:"string",next:"@codeblock"}],{include:"@linecontent"}],table_header:[{include:"@table_common"},[/[^\|]+/,"keyword.table.header"]],table_body:[{include:"@table_common"},{include:"@linecontent"}],table_common:[[/\s*[\-:]+\s*/,{token:"keyword",switchTo:"table_body"}],[/^\s*\|/,"keyword.table.left"],[/^\s*[^\|]/,"@rematch","@pop"],[/^\s*$/,"@rematch","@pop"],[/\|/,{cases:{"@eos":"keyword.table.right","@default":"keyword.table.middle"}}]],codeblock:[[/^\s*~~~\s*$/,{token:"string",next:"@pop"}],[/^\s*```\s*$/,{token:"string",next:"@pop"}],[/.*$/,"variable.source"]],codeblockgh:[[/```\s*$/,{token:"string",next:"@pop",nextEmbedded:"@pop"}],[/[^`]+/,"variable.source"]],linecontent:[[/&\w+;/,"string.escape"],[/@escapes/,"escape"],[/\b__([^\\_]|@escapes|_(?!_))+__\b/,"strong"],[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/,"strong"],[/\b_[^_]+_\b/,"emphasis"],[/\*([^\\*]|@escapes)+\*/,"emphasis"],[/`([^\\`]|@escapes)+`/,"variable"],[/\{+[^}]+\}+/,"string.target"],[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,["string.link","","string.link"]],[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/,"string.link"],{include:"html"}],html:[[/<(\w+)\/>/,"tag"],[/<(\w+)(\-|\w)*/,{cases:{"@empty":{token:"tag",next:"@tag.$1"},"@default":{token:"tag",next:"@tag.$1"}}}],[/<\/(\w+)(\-|\w)*\s*>/,{token:"tag"}],[//,"comment","@pop"],[/ ## Summary by CodeRabbit * **Chores** * Updated Payload CMS dependencies to version 3.74.0, including enhancements to the admin interface, database support, and live preview functionality. --- apps/cms/package.json | 6 +- pnpm-lock.yaml | 1535 +++++++++++++++++++++++++++++++---------- 2 files changed, 1191 insertions(+), 350 deletions(-) diff --git a/apps/cms/package.json b/apps/cms/package.json index cf248894b90..da9b2ce3be4 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -20,9 +20,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@payloadcms/admin-bar": "^3.52.0", - "@payloadcms/db-postgres": "^3.52.0", - "@payloadcms/live-preview-react": "^3.52.0", + "@payloadcms/admin-bar": "^3.74.0", + "@payloadcms/db-postgres": "^3.74.0", + "@payloadcms/live-preview-react": "^3.74.0", "@payloadcms/next": "3.52.0", "@payloadcms/payload-cloud": "3.60.0", "@payloadcms/plugin-form-builder": "3.52.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73db6c42f31..5e94fb5640d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,14 +141,14 @@ importers: apps/cms: dependencies: '@payloadcms/admin-bar': - specifier: ^3.52.0 - version: 3.54.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^3.74.0 + version: 3.74.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@payloadcms/db-postgres': - specifier: ^3.52.0 - version: 3.54.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(supports-color@8.1.1) + specifier: ^3.74.0 + version: 3.74.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(supports-color@8.1.1) '@payloadcms/live-preview-react': - specifier: ^3.52.0 - version: 3.54.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^3.74.0 + version: 3.74.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@payloadcms/next': specifier: 3.52.0 version: 3.52.0(@types/react@18.3.3)(graphql@16.11.0)(monaco-editor@0.52.2)(next@15.5.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1)(typescript@5.9.2) @@ -262,7 +262,7 @@ importers: version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) contentlayer2: specifier: 0.4.6 - version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + version: 0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -289,7 +289,7 @@ importers: version: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.12)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -768,13 +768,13 @@ importers: version: 5.1.3 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) apps/learn: dependencies: @@ -867,7 +867,7 @@ importers: version: 1.2.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) + version: 7.4.0(@react-router/dev@7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) '@supabase/postgrest-js': specifier: 'catalog:' version: 2.94.1 @@ -897,7 +897,7 @@ importers: version: 1.8.2 contentlayer2: specifier: 0.4.6 - version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + version: 0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) eslint-config-supabase: specifier: workspace:* version: link:../../packages/eslint-config-supabase @@ -918,7 +918,7 @@ importers: version: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.12)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -988,7 +988,7 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.1.5 - version: 7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + version: 7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1003,7 +1003,7 @@ importers: version: 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-start': specifier: ^1.114.25 - version: 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1) + version: 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@25.1.0)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))(yaml@2.8.1) '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 @@ -1042,13 +1042,13 @@ importers: version: 4.4.1 shadcn: specifier: ^3.0.0 - version: 3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) + version: 3.3.1(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) shiki: specifier: ^1.1.7 version: 1.6.0 tailwindcss: specifier: 'catalog:' - version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + version: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -1060,7 +1060,7 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) apps/studio: dependencies: @@ -1204,7 +1204,7 @@ importers: version: 2.1.0(@aws-sdk/credential-provider-web-identity@3.830.0) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) '@zip.js/zip.js': specifier: ^2.7.29 version: 2.7.30 @@ -1547,7 +1547,7 @@ importers: version: link:../../packages/eslint-config-supabase 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)) + version: 2.0.7(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) graphql-ws: specifier: 5.14.1 version: 5.14.1(graphql@16.11.0) @@ -1592,13 +1592,13 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) apps/ui-library: dependencies: @@ -1691,7 +1691,7 @@ importers: version: 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) + version: 7.4.0(@react-router/dev@7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) '@supabase/postgrest-js': specifier: 'catalog:' version: 2.94.1 @@ -1721,7 +1721,7 @@ importers: version: 1.8.2 contentlayer2: specifier: 0.4.6 - version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + version: 0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) eslint-config-supabase: specifier: workspace:* version: link:../../packages/eslint-config-supabase @@ -1742,7 +1742,7 @@ importers: version: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.12)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1809,7 +1809,7 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.9.0 - version: 7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + version: 7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1824,7 +1824,7 @@ importers: version: 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-start': specifier: ^1.114.25 - version: 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1) + version: 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@25.1.0)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))(yaml@2.8.1) '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 @@ -1860,13 +1860,13 @@ importers: version: 4.4.1 shadcn: specifier: ^3.0.0 - version: 3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) + version: 3.3.1(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) shiki: specifier: ^1.1.7 version: 1.6.0 tailwindcss: specifier: 'catalog:' - version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + version: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -1878,7 +1878,7 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) apps/www: dependencies: @@ -2191,7 +2191,7 @@ importers: version: 0.562.0(vue@3.5.21(typescript@5.9.2)) nuxt: specifier: ^4.0.3 - version: 4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + version: 4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@25.1.0)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -2204,13 +2204,13 @@ importers: devDependencies: shadcn: specifier: ^3.0.0 - version: 3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) + version: 3.3.1(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) e2e/studio: dependencies: @@ -2308,10 +2308,10 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages/api-types: devDependencies: @@ -2417,7 +2417,7 @@ importers: version: 5.9.2 vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages/config: dependencies: @@ -2509,22 +2509,22 @@ importers: version: 9.37.0 '@tanstack/eslint-plugin-query': specifier: ^5.0.0 - version: 5.91.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + version: 5.91.2(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/eslint-plugin': specifier: ^8.48.0 - version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.48.0 - version: 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + version: 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) eslint-config-next: specifier: ^15.5.0 - version: 15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + version: 15.5.4(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) eslint-config-prettier: specifier: ^10.0.0 - version: 10.1.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + version: 10.1.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) eslint-config-turbo: specifier: ^2.5.0 - version: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) + version: 2.5.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) typescript: specifier: 'catalog:' version: 5.9.2 @@ -2595,10 +2595,10 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages/shared-data: dependencies: @@ -2848,10 +2848,10 @@ importers: version: 5.9.2 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages/ui-patterns: dependencies: @@ -3050,10 +3050,10 @@ importers: version: 6.0.3 vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages: @@ -3421,6 +3421,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.4': resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} engines: {node: '>=6.9.0'} @@ -3495,6 +3499,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -3565,6 +3573,10 @@ packages: resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} @@ -3913,150 +3925,306 @@ packages: peerDependencies: esbuild: ^0.25.2 + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.2': resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.2': resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.2': resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.2': resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.2': resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.2': resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.2': resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.2': resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.2': resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.2': resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.2': resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.2': resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.2': resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.2': resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.2': resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.2': resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.2': resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.2': resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.2': resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.2': resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.2': resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.2': resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.2': resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.2': resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.2': resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} engines: {node: '>=18'} @@ -4069,38 +4237,76 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.21.0': resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.0': resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.16.0': resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.37.0': resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.0': resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} @@ -4942,6 +5148,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} @@ -6388,21 +6597,21 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@payloadcms/admin-bar@3.54.0': - resolution: {integrity: sha512-aBwwt2+C19uIaKgfeKd9DgMLQyf0GWKHs1rTkk5xtiNVUzHfazbQoSu54FivgXLTyN+zGr5q44acfNdsTZFPxw==} + '@payloadcms/admin-bar@3.74.0': + resolution: {integrity: sha512-cSbYdWkUrEeCRZluZrD/ZvhZI1wXZZbzLD9BoWoKZ3+4b/PHCqQ/XOdsLqL4snndoI+YN+JWLaM/Ilnzng+Ocw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1 - '@payloadcms/db-postgres@3.54.0': - resolution: {integrity: sha512-C1tADNHHRTfXyJ6UXTPrHhDVji8kQFKPmT3R/LxOfXwMX5SQanfGx4dGotug647EuZCLgP9YH/ASmGgSFG2RjQ==} + '@payloadcms/db-postgres@3.74.0': + resolution: {integrity: sha512-39wwFY1tooHrS7M8ycAtIOvRRQqRJfjzD3kBlv4oI9q+Xhs0dvCBcIsgwiFy+4eXC1rkOAyTnWk4l0vVagePbQ==} peerDependencies: - payload: 3.54.0 + payload: 3.74.0 - '@payloadcms/drizzle@3.54.0': - resolution: {integrity: sha512-nz3luQKWav/CGfPKIRMl9lwmgs11HYWvlTpsGUnni8/cJl4EZgi05/Z4yLz3T6RJLbzyCpqvV1D+8tmdRPIHBw==} + '@payloadcms/drizzle@3.74.0': + resolution: {integrity: sha512-GjlOnAPALhECzzuU8zp6TBG+Sd0MEhBdU3KKzCaUkupwSef68KrNfvX9muVpqiCJTUo7q4Yievo80YkFh/fIYw==} peerDependencies: - payload: 3.54.0 + payload: 3.74.0 '@payloadcms/email-nodemailer@3.60.0': resolution: {integrity: sha512-9yU2oU4eYTbrMYUNO9P3o7RQgTgt2fpfpuYVSynlOokR/vRnNBRN3k/W6oEmPjfQylybtbY/gnda1I/4KL8NYg==} @@ -6423,9 +6632,18 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc-65a56d0e-20241020 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc-65a56d0e-20241020 + '@payloadcms/live-preview-react@3.74.0': + resolution: {integrity: sha512-/ItgTm+xlHT0tlv93Dmyb/NXB9N1rqTRzd4PfA45ZVN392xl6JtjfjKbafJ0inRBjK6eE8J73DVfmDvJMZV7NA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1 + '@payloadcms/live-preview@3.54.0': resolution: {integrity: sha512-BJjgrbOe/lnt3RNjwtpzv23n1snEKSYhpEupYyhSuImWTQ4FfppgyxYLLdtoGP0kbwhFN1VP2+sZtfbaW42F7w==} + '@payloadcms/live-preview@3.74.0': + resolution: {integrity: sha512-pp77TUCSajma2ml9d0oxgaRf4PySDhkzn7bK9OrjPBI+mAYC72K7FwAAeBYidn8REfgaCwS1hMAv52IUskxRWA==} + '@payloadcms/next@3.52.0': resolution: {integrity: sha512-Cy23EDAMatFB/hx17DhG7RmFFVmacuoNZIlEEkjrTHk+XAQW1TAGBch7TcuFGbzciwvt6pb/MuiPbe3T0/hgqA==} engines: {node: ^18.20.2 || >=20.9.0} @@ -9426,8 +9644,8 @@ packages: '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -9453,6 +9671,9 @@ packages: '@types/aria-query@5.0.2': resolution: {integrity: sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -9738,6 +9959,9 @@ packages: '@types/node@22.13.14': resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} + '@types/node@25.1.0': + resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} + '@types/papaparse@5.3.9': resolution: {integrity: sha512-sZcrKD63qA4/6GyBcVvX6AIp0AkpfyYk00CUQHMBvb4+OVXTZWyXUvidUZaai1wyKUVyJoxO7mgREam/pMRrDw==} @@ -10656,6 +10880,10 @@ packages: resolution: {integrity: sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==} hasBin: true + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -10744,6 +10972,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -10844,6 +11077,9 @@ packages: caniuse-lite@1.0.30001743: resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -11800,8 +12036,8 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decko@1.2.0: resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==} @@ -12046,12 +12282,12 @@ packages: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} - drizzle-kit@0.31.4: - resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==} + drizzle-kit@0.31.7: + resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} hasBin: true - drizzle-orm@0.44.2: - resolution: {integrity: sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ==} + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -12174,6 +12410,9 @@ packages: electron-to-chromium@1.5.221: resolution: {integrity: sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==} + electron-to-chromium@1.5.283: + resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -12208,6 +12447,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + entities@2.1.0: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} @@ -12215,6 +12458,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -12290,6 +12537,11 @@ packages: peerDependencies: esbuild: ^0.25.2 + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -12526,6 +12778,16 @@ packages: jiti: optional: true + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} @@ -12543,6 +12805,10 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -12971,6 +13237,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -13166,6 +13436,9 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.13.1: + resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -13210,7 +13483,7 @@ packages: glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob 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 glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -13730,6 +14003,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-from@4.0.0: resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} engines: {node: '>=12.2'} @@ -14512,8 +14789,8 @@ packages: lexical@0.28.0: resolution: {integrity: sha512-dLE3O1PZg0TlZxRQo9YDpjCjDUj8zluGyBO9MHdjo21qZmMUNrxQPeCRt8fn2s5l4HKYFQ1YNgl7k1pOJB/vZQ==} - lib0@0.2.108: - resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==} + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} engines: {node: '>=16'} hasBin: true @@ -14573,8 +14850,8 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} loader-utils@1.4.2: @@ -15731,6 +16008,9 @@ packages: node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-sql-parser@4.18.0: resolution: {integrity: sha512-2YEOR5qlI1zUFbGMLKNfsrR5JUvFg9LxIRVE+xJe962pfVLH0rnItqLzv96XVs1Y1UIR8FxsXAuvX/lYAWZ2BQ==} engines: {node: '>=8'} @@ -15892,8 +16172,8 @@ packages: '@types/node': optional: true - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} @@ -16082,6 +16362,10 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -16211,8 +16495,8 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -16945,8 +17229,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -17821,8 +18105,8 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - schema-utils@4.3.0: - resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} scmp@2.1.0: @@ -18597,6 +18881,10 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -18611,8 +18899,8 @@ packages: resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} engines: {node: '>=12'} - terser-webpack-plugin@5.3.14: - resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + terser-webpack-plugin@5.3.16: + resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -18632,6 +18920,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.46.0: + resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + engines: {node: '>=10'} + hasBin: true + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -19076,6 +19369,9 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@6.23.0: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} @@ -19315,6 +19611,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} @@ -19720,8 +20022,8 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - watchpack@2.4.2: - resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} wcwidth@1.0.1: @@ -19757,6 +20059,10 @@ packages: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + webpack-virtual-modules@0.5.0: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} @@ -19849,6 +20155,10 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -19899,6 +20209,18 @@ packages: utf-8-validate: optional: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -21052,6 +21374,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.4': {} '@babel/core@7.28.4(supports-color@8.1.1)': @@ -21166,6 +21494,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': @@ -21240,6 +21570,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.0 + '@babel/runtime@7.28.6': {} + '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.27.1 @@ -21438,9 +21770,9 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@contentlayer2/cli@0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)': + '@contentlayer2/cli@0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1)': dependencies: - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 clipanion: 3.2.1(typanion@3.14.0) typanion: 3.14.0 @@ -21450,22 +21782,22 @@ snapshots: - markdown-wasm - supports-color - '@contentlayer2/client@0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)': + '@contentlayer2/client@0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1)': dependencies: - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) transitivePeerDependencies: - '@effect-ts/otel-node' - esbuild - markdown-wasm - supports-color - '@contentlayer2/core@0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)': + '@contentlayer2/core@0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1)': dependencies: '@contentlayer2/utils': 0.4.3 camel-case: 4.1.2 comment-json: 4.2.3 gray-matter: 4.0.3 - mdx-bundler: 10.0.2(esbuild@0.25.2)(supports-color@8.1.1) + mdx-bundler: 10.0.2(esbuild@0.25.12)(supports-color@8.1.1) rehype-stringify: 10.0.0 remark-frontmatter: 5.0.0(supports-color@8.1.1) remark-parse: 11.0.0(supports-color@8.1.1) @@ -21474,15 +21806,15 @@ snapshots: type-fest: 4.30.0 unified: 11.0.5 optionalDependencies: - esbuild: 0.25.2 + esbuild: 0.25.12 markdown-wasm: 1.2.0 transitivePeerDependencies: - '@effect-ts/otel-node' - supports-color - '@contentlayer2/source-files@0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)': + '@contentlayer2/source-files@0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1)': dependencies: - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 chokidar: 3.6.0 fast-glob: 3.3.3 @@ -21499,10 +21831,10 @@ snapshots: - markdown-wasm - supports-color - '@contentlayer2/source-remote-files@0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)': + '@contentlayer2/source-remote-files@0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1)': dependencies: - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) - '@contentlayer2/source-files': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/source-files': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 transitivePeerDependencies: - '@effect-ts/otel-node' @@ -21767,96 +22099,174 @@ snapshots: '@esbuild-kit/core-utils@3.3.2': dependencies: - esbuild: 0.25.2 + esbuild: 0.25.12 source-map-support: 0.5.21 '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.10.0 + get-tsconfig: 4.13.1 - '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.2)(supports-color@8.1.1)': + '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.12)(supports-color@8.1.1)': dependencies: '@types/resolve': 1.20.6 debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.25.2 + esbuild: 0.25.12 escape-string-regexp: 4.0.0 resolve: 1.22.10 transitivePeerDependencies: - supports-color + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.25.2': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.25.2': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.25.2': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.25.2': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.25.2': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.25.2': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.25.2': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.25.2': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.25.2': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.25.2': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.25.2': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.25.2': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.25.2': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.25.2': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.25.2': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.25.2': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.25.2': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.25.2': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.25.2': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.25.2': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.25.2': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.25.2': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.25.2': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.25.2': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.25.2': optional: true @@ -21865,8 +22275,20 @@ snapshots: eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.21.0(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.6 @@ -21875,14 +22297,30 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-array@0.21.1(supports-color@8.1.1)': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + '@eslint/config-helpers@0.4.0': dependencies: '@eslint/core': 0.16.0 + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + '@eslint/core@0.16.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1(supports-color@8.1.1)': dependencies: ajv: 6.12.6 @@ -21897,15 +22335,38 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/eslintrc@3.3.3(supports-color@8.1.1)': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + '@eslint/js@9.37.0': {} + '@eslint/js@9.39.2': {} + '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} + '@eslint/plugin-kit@0.4.0': dependencies: '@eslint/core': 0.16.0 levn: 0.4.1 + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@exodus/schemasafe@1.3.0': {} '@faceless-ui/modal@3.0.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -22839,6 +23300,13 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + '@inquirer/confirm@5.1.8(@types/node@25.1.0)': + dependencies: + '@inquirer/core': 10.1.9(@types/node@25.1.0) + '@inquirer/type': 3.0.5(@types/node@25.1.0) + optionalDependencies: + '@types/node': 25.1.0 + '@inquirer/core@10.1.9(@types/node@22.13.14)': dependencies: '@inquirer/figures': 1.0.11 @@ -22852,6 +23320,19 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + '@inquirer/core@10.1.9(@types/node@25.1.0)': + dependencies: + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.10(@types/node@25.1.0) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 25.1.0 + '@inquirer/core@10.3.2(@types/node@22.13.14)': dependencies: '@inquirer/ansi': 1.0.2 @@ -22960,10 +23441,18 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + '@inquirer/type@3.0.10(@types/node@25.1.0)': + optionalDependencies: + '@types/node': 25.1.0 + '@inquirer/type@3.0.5(@types/node@22.13.14)': optionalDependencies: '@types/node': 22.13.14 + '@inquirer/type@3.0.5(@types/node@25.1.0)': + optionalDependencies: + '@types/node': 25.1.0 + '@ioredis/commands@1.4.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -23003,6 +23492,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/source-map@0.3.6': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -23277,11 +23771,11 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/esbuild@3.0.1(esbuild@0.25.2)(supports-color@8.1.1)': + '@mdx-js/esbuild@3.0.1(esbuild@0.25.12)(supports-color@8.1.1)': dependencies: '@mdx-js/mdx': 3.0.1(supports-color@8.1.1) '@types/unist': 3.0.3 - esbuild: 0.25.2 + esbuild: 0.25.12 vfile: 6.0.3 vfile-message: 4.0.2 transitivePeerDependencies: @@ -23713,11 +24207,11 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@2.6.4(magicast@0.3.5)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': + '@nuxt/devtools-kit@2.6.4(magicast@0.3.5)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@nuxt/kit': 3.19.2(magicast@0.3.5) execa: 8.0.1 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - magicast @@ -23732,12 +24226,12 @@ snapshots: prompts: 2.4.2 semver: 7.7.3 - '@nuxt/devtools@2.6.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@nuxt/devtools@2.6.4(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: - '@nuxt/devtools-kit': 2.6.4(magicast@0.3.5)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + '@nuxt/devtools-kit': 2.6.4(magicast@0.3.5)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) '@nuxt/devtools-wizard': 2.6.4 '@nuxt/kit': 3.19.2(magicast@0.3.5) - '@vue/devtools-core': 7.7.7(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@vue/devtools-core': 7.7.7(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@vue/devtools-kit': 7.7.7 birpc: 2.5.0 consola: 3.4.2 @@ -23762,9 +24256,9 @@ snapshots: sirv: 3.0.2 structured-clone-es: 1.0.0 tinyglobby: 0.2.15 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) - vite-plugin-vue-tracer: 1.0.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-plugin-vue-tracer: 1.0.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) which: 5.0.0 ws: 8.18.3 transitivePeerDependencies: @@ -23855,12 +24349,12 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.1.2(@types/node@22.13.14)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': + '@nuxt/vite-builder@4.1.2(@types/node@25.1.0)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.1.2(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.50.2) - '@vitejs/plugin-vue': 6.0.1(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) - '@vitejs/plugin-vue-jsx': 5.1.1(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@vitejs/plugin-vue': 6.0.1(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@vitejs/plugin-vue-jsx': 5.1.1(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) autoprefixer: 10.4.21(postcss@8.5.6) consola: 3.4.2 cssnano: 7.1.1(postcss@8.5.6) @@ -23882,9 +24376,9 @@ snapshots: std-env: 3.9.0 ufo: 1.6.3 unenv: 2.0.0-rc.21 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-plugin-checker: 0.10.3(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-checker: 0.10.3(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) vue: 3.5.21(typescript@5.9.2) vue-bundle-renderer: 2.1.2 transitivePeerDependencies: @@ -24676,18 +25170,18 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 - '@payloadcms/admin-bar@3.54.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@payloadcms/admin-bar@3.74.0(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) - '@payloadcms/db-postgres@3.54.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(supports-color@8.1.1)': + '@payloadcms/db-postgres@3.74.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: - '@payloadcms/drizzle': 3.54.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(pg@8.16.3) + '@payloadcms/drizzle': 3.74.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(pg@8.16.3) '@types/pg': 8.10.2 console-table-printer: 2.12.1 - drizzle-kit: 0.31.4(supports-color@8.1.1) - drizzle-orm: 0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3) + drizzle-kit: 0.31.7(supports-color@8.1.1) + drizzle-orm: 0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3) payload: 3.52.0(graphql@16.11.0)(typescript@5.9.2) pg: 8.16.3 prompts: 2.4.2 @@ -24724,11 +25218,11 @@ snapshots: - sqlite3 - supports-color - '@payloadcms/drizzle@3.54.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(pg@8.16.3)': + '@payloadcms/drizzle@3.74.0(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(pg@8.16.3)': dependencies: console-table-printer: 2.12.1 dequal: 2.0.3 - drizzle-orm: 0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3) + drizzle-orm: 0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3) payload: 3.52.0(graphql@16.11.0)(typescript@5.9.2) prompts: 2.4.2 to-snake-case: 1.0.0 @@ -24786,8 +25280,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@payloadcms/live-preview-react@3.74.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@payloadcms/live-preview': 3.74.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@payloadcms/live-preview@3.54.0': {} + '@payloadcms/live-preview@3.74.0': {} + '@payloadcms/next@3.52.0(@types/react@18.3.3)(graphql@16.11.0)(monaco-editor@0.52.2)(next@15.5.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(payload@3.52.0(graphql@16.11.0)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@dnd-kit/core': 6.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -26805,7 +27307,7 @@ snapshots: dependencies: react: 18.3.1 - '@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1)': + '@react-router/dev@7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@babel/core': 7.28.4(supports-color@8.1.1) '@babel/generator': 7.28.3 @@ -26835,10 +27337,10 @@ snapshots: semver: 7.7.3 tinyglobby: 0.2.15 valibot: 1.2.0(typescript@5.9.2) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) optionalDependencies: - '@vitejs/plugin-rsc': 0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitejs/plugin-rsc': 0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) typescript: 5.9.2 transitivePeerDependencies: - '@types/node' @@ -26856,9 +27358,9 @@ snapshots: - tsx - yaml - '@react-router/fs-routes@7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2)': + '@react-router/fs-routes@7.4.0(@react-router/dev@7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2)': dependencies: - '@react-router/dev': 7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + '@react-router/dev': 7.9.6(@types/node@25.1.0)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) minimatch: 9.0.5 optionalDependencies: typescript: 5.9.2 @@ -28060,7 +28562,7 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) - '@tanstack/directive-functions-plugin@1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/directive-functions-plugin@1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.4(supports-color@8.1.1) @@ -28073,7 +28575,7 @@ snapshots: babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) dedent: 1.7.0(babel-plugin-macros@3.1.0) tiny-invariant: 1.3.3 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -28089,10 +28591,10 @@ snapshots: - tsx - yaml - '@tanstack/eslint-plugin-query@5.91.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@typescript-eslint/utils': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - typescript @@ -28125,7 +28627,7 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-client@1.114.27(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/react-start-client@1.114.27(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-core': 1.114.25 @@ -28136,7 +28638,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28180,22 +28682,22 @@ snapshots: - xml2js - yaml - '@tanstack/react-start-config@1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1)': + '@tanstack/react-start-config@1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@25.1.0)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))(yaml@2.8.1)': dependencies: - '@tanstack/react-start-plugin': 1.114.12(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/react-start-plugin': 1.114.12(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/router-core': 1.114.25 '@tanstack/router-generator': 1.114.27(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) - '@tanstack/router-plugin': 1.114.27(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2)) - '@tanstack/server-functions-plugin': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/router-plugin': 1.114.27(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12)) + '@tanstack/server-functions-plugin': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/start-server-functions-handler': 1.114.25 - '@vitejs/plugin-react': 4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitejs/plugin-react': 4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) import-meta-resolve: 4.1.0 - nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) + nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) ofetch: 1.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -28245,7 +28747,7 @@ snapshots: - xml2js - yaml - '@tanstack/react-start-plugin@1.114.12(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/react-start-plugin@1.114.12(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.4(supports-color@8.1.1) @@ -28257,7 +28759,7 @@ snapshots: '@tanstack/router-utils': 1.114.12 babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) tiny-invariant: 1.3.3 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -28272,11 +28774,11 @@ snapshots: - tsx - yaml - '@tanstack/react-start-router-manifest@1.114.25(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/react-start-router-manifest@1.114.25(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.114.25 tiny-invariant: 1.3.3 - vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28335,20 +28837,20 @@ snapshots: tiny-warning: 1.0.3 unctx: 2.4.1 - '@tanstack/react-start@1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1)': + '@tanstack/react-start@1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@25.1.0)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))(yaml@2.8.1)': dependencies: - '@tanstack/react-start-client': 1.114.27(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - '@tanstack/react-start-config': 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1) - '@tanstack/react-start-router-manifest': 1.114.25(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/react-start-client': 1.114.27(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/react-start-config': 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@25.1.0)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))(yaml@2.8.1) + '@tanstack/react-start-router-manifest': 1.114.25(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/react-start-server': 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/start-api-routes': 1.114.25(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - '@tanstack/start-server-functions-client': 1.114.25(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/start-api-routes': 1.114.25(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/start-server-functions-client': 1.114.25(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/start-server-functions-handler': 1.114.25 - '@tanstack/start-server-functions-server': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - '@tanstack/start-server-functions-ssr': 1.114.25(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/start-server-functions-server': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/start-server-functions-ssr': 1.114.25(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28431,7 +28933,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.114.27(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))': + '@tanstack/router-plugin@1.114.27(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.12))': dependencies: '@babel/core': 7.28.4(supports-color@8.1.1) '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4(supports-color@8.1.1)) @@ -28452,8 +28954,8 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - webpack: 5.94.0(esbuild@0.25.2) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + webpack: 5.94.0(esbuild@0.25.12) transitivePeerDependencies: - supports-color @@ -28464,7 +28966,7 @@ snapshots: ansis: 3.17.0 diff: 7.0.0 - '@tanstack/server-functions-plugin@1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/server-functions-plugin@1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.4(supports-color@8.1.1) @@ -28473,7 +28975,7 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.4(supports-color@8.1.1) '@babel/types': 7.28.4 - '@tanstack/directive-functions-plugin': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/directive-functions-plugin': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) dedent: 1.7.0(babel-plugin-macros@3.1.0) tiny-invariant: 1.3.3 @@ -28492,11 +28994,11 @@ snapshots: - tsx - yaml - '@tanstack/start-api-routes@1.114.25(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/start-api-routes@1.114.25(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.114.25 '@tanstack/start-server-core': 1.114.25 - vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vinxi: 0.5.3(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -28558,9 +29060,9 @@ snapshots: tiny-warning: 1.0.3 unctx: 2.4.1 - '@tanstack/start-server-functions-client@1.114.25(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/start-server-functions-client@1.114.25(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: - '@tanstack/server-functions-plugin': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/server-functions-plugin': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/start-server-functions-fetcher': 1.114.25 transitivePeerDependencies: - '@types/node' @@ -28589,9 +29091,9 @@ snapshots: '@tanstack/start-server-core': 1.114.25 tiny-invariant: 1.3.3 - '@tanstack/start-server-functions-server@1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/start-server-functions-server@1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: - '@tanstack/server-functions-plugin': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/server-functions-plugin': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) tiny-invariant: 1.3.3 transitivePeerDependencies: - '@types/node' @@ -28608,9 +29110,9 @@ snapshots: - tsx - yaml - '@tanstack/start-server-functions-ssr@1.114.25(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)': + '@tanstack/start-server-functions-ssr@1.114.25(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: - '@tanstack/server-functions-plugin': 1.114.12(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@tanstack/server-functions-plugin': 1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@tanstack/start-client-core': 1.114.25 '@tanstack/start-server-core': 1.114.25 '@tanstack/start-server-functions-fetcher': 1.114.25 @@ -28651,9 +29153,9 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.26.10 - '@types/aria-query': 5.0.2 + '@babel/code-frame': 7.28.6 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 dom-accessibility-api: 0.5.16 @@ -28718,7 +29220,7 @@ snapshots: minimatch: 10.1.1 path-browserify: 1.0.1 - '@tsconfig/node10@1.0.11': + '@tsconfig/node10@1.0.12': optional: true '@tsconfig/node12@1.0.11': @@ -28745,6 +29247,8 @@ snapshots: '@types/aria-query@5.0.2': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -29062,6 +29566,10 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/node@25.1.0': + dependencies: + undici-types: 7.16.0 + '@types/papaparse@5.3.9': dependencies: '@types/node': 22.13.14 @@ -29216,15 +29724,15 @@ snapshots: '@types/zxcvbn@4.4.2': {} - '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/type-utils': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/utils': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.48.0 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -29233,14 +29741,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.48.0 '@typescript-eslint/types': 8.48.0 '@typescript-eslint/typescript-estree': 8.48.0(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.48.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -29263,13 +29771,13 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.48.0 '@typescript-eslint/typescript-estree': 8.48.0(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/utils': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -29292,13 +29800,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/utils@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.48.0 '@typescript-eslint/types': 8.48.0 '@typescript-eslint/typescript-estree': 8.48.0(supports-color@8.1.1)(typescript@5.9.2) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -29391,18 +29899,29 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 - '@vitejs/plugin-react@4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': + '@vitejs/plugin-react@4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4(supports-color@8.1.1) '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.28.4(supports-color@8.1.1)) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.28.4(supports-color@8.1.1)) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': + '@vitejs/plugin-react@4.3.4(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.28.4(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.28.4(supports-color@8.1.1)) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: es-module-lexer: 2.0.0 estree-walker: 3.0.3 @@ -29413,26 +29932,26 @@ snapshots: srvx: 0.9.8 strip-literal: 3.1.0 turbo-stream: 3.1.0 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) optional: true - '@vitejs/plugin-vue-jsx@5.1.1(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vitejs/plugin-vue-jsx@5.1.1(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@babel/core': 7.28.4(supports-color@8.1.1) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4(supports-color@8.1.1)) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4(supports-color@8.1.1))(supports-color@8.1.1) '@rolldown/pluginutils': 1.0.0-beta.9-commit.d91dfb5 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4(supports-color@8.1.1))(supports-color@8.1.1) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.1(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vitejs/plugin-vue@6.0.1(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) '@vitest/coverage-v8@3.2.4(supports-color@8.1.1)(vitest@3.2.4)': @@ -29450,7 +29969,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -29462,14 +29981,23 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.11.3(@types/node@22.13.14)(typescript@5.9.2) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + + '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.11.3(@types/node@25.1.0)(typescript@5.9.2) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -29500,7 +30028,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -29590,14 +30118,14 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-core@7.7.7(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vue/devtools-core@7.7.7(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-hot-client: 2.1.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) vue: 3.5.21(typescript@5.9.2) transitivePeerDependencies: - vite @@ -30226,6 +30754,8 @@ snapshots: baseline-browser-mapping@2.8.5: {} + baseline-browser-mapping@2.9.19: {} + before-after-hook@3.0.2: {} better-ajv-errors@1.2.0(ajv@8.17.1): @@ -30380,6 +30910,14 @@ snapshots: node-releases: 2.0.21 update-browserslist-db: 1.1.3(browserslist@4.26.2) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.283 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -30521,6 +31059,8 @@ snapshots: caniuse-lite@1.0.30001743: {} + caniuse-lite@1.0.30001766: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -30988,13 +31528,13 @@ snapshots: content-type@1.0.5: {} - contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1): + contentlayer2@0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1): dependencies: - '@contentlayer2/cli': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) - '@contentlayer2/client': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) - '@contentlayer2/source-files': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) - '@contentlayer2/source-remote-files': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/cli': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/client': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/source-files': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/source-remote-files': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 transitivePeerDependencies: - '@effect-ts/otel-node' @@ -31500,10 +32040,10 @@ snapshots: dayjs@1.11.18: {} - db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)): + db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)): optionalDependencies: '@electric-sql/pglite': 0.2.15 - drizzle-orm: 0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3) + drizzle-orm: 0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3) de-indent@1.0.2: {} @@ -31543,7 +32083,7 @@ snapshots: decimal.js-light@2.5.1: {} - decimal.js@10.5.0: + decimal.js@10.6.0: optional: true decko@1.2.0: {} @@ -31736,23 +32276,23 @@ snapshots: drange@1.1.1: {} - drizzle-kit@0.31.4(supports-color@8.1.1): + drizzle-kit@0.31.7(supports-color@8.1.1): dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.2 - esbuild-register: 3.6.0(esbuild@0.25.2)(supports-color@8.1.1) + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3): + drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(pg@8.16.3): optionalDependencies: '@electric-sql/pglite': 0.2.15 '@opentelemetry/api': 1.9.0 '@types/pg': 8.10.2 pg: 8.16.3 - drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3): + drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3): optionalDependencies: '@electric-sql/pglite': 0.2.15 '@opentelemetry/api': 1.9.0 @@ -31789,6 +32329,8 @@ snapshots: electron-to-chromium@1.5.221: {} + electron-to-chromium@1.5.283: {} + emoji-regex@10.3.0: {} emoji-regex@8.0.0: {} @@ -31817,10 +32359,18 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@2.1.0: {} entities@4.5.0: {} + entities@6.0.1: + optional: true + env-paths@2.2.1: {} err-code@2.0.3: {} @@ -31963,13 +32513,42 @@ snapshots: d: 1.0.2 ext: 1.7.0 - esbuild-register@3.6.0(esbuild@0.25.2)(supports-color@8.1.1): + esbuild-register@3.6.0(esbuild@0.25.12)(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.25.2 + esbuild: 0.25.12 transitivePeerDependencies: - supports-color + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.25.2: optionalDependencies: '@esbuild/aix-ppc64': 0.25.2 @@ -32074,33 +32653,33 @@ snapshots: eslint-barrel-file-utils-win32-ia32-msvc: 0.0.10 eslint-barrel-file-utils-win32-x64-msvc: 0.0.10 - eslint-config-next@15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2): + eslint-config-next@15.5.4(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2): dependencies: '@next/eslint-plugin-next': 15.5.4 '@rushstack/eslint-patch': 1.10.3 - '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/parser': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) - eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color - eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) - eslint-config-turbo@2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): + eslint-config-turbo@2.5.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): dependencies: - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) - eslint-plugin-turbo: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) + eslint-plugin-turbo: 2.5.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) turbo: 2.3.3 eslint-import-resolver-node@0.3.9(supports-color@8.1.1): @@ -32111,13 +32690,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) enhanced-resolve: 5.18.1 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) fast-glob: 3.3.3 get-tsconfig: 4.10.0 is-core-module: 2.16.1 @@ -32128,24 +32707,24 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-plugin-barrel-files@2.0.7(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): + eslint-plugin-barrel-files@2.0.7(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) eslint-barrel-file-utils: 0.0.10 requireindex: 1.2.0 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -32154,9 +32733,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -32168,13 +32747,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.48.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -32184,7 +32763,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -32193,11 +32772,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) - eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -32205,7 +32784,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -32219,10 +32798,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): + eslint-plugin-turbo@2.5.8(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): dependencies: dotenv: 16.0.3 - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) turbo: 2.3.3 eslint-scope@5.1.1: @@ -32281,6 +32860,47 @@ snapshots: transitivePeerDependencies: - supports-color + eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1(supports-color@8.1.1) + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3(supports-color@8.1.1) + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.5.1 + transitivePeerDependencies: + - supports-color + esniff@2.0.1: dependencies: d: 1.0.2 @@ -32300,6 +32920,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -32843,6 +33467,15 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + optional: true + format@0.2.2: {} formdata-node@4.4.1: @@ -33043,6 +33676,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.13.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -33800,6 +34437,11 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-from@4.0.0: {} import-in-the-middle@2.0.0: @@ -34284,7 +34926,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.13.14 + '@types/node': 25.1.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -34349,16 +34991,16 @@ snapshots: cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 - decimal.js: 10.5.0 + decimal.js: 10.6.0 domexception: 4.0.0 escodegen: 2.1.0 - form-data: 4.0.4 + form-data: 4.0.5 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0(supports-color@8.1.1) https-proxy-agent: 5.0.1(supports-color@8.1.1) is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.20 - parse5: 7.2.1 + nwsapi: 2.2.23 + parse5: 7.3.0 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 4.1.4 @@ -34367,7 +35009,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -34541,7 +35183,7 @@ snapshots: lexical@0.28.0: {} - lib0@0.2.108: + lib0@0.2.117: dependencies: isomorphic.js: 0.2.5 @@ -34647,7 +35289,7 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 - loader-runner@4.3.0: {} + loader-runner@4.3.1: {} loader-utils@1.4.2: dependencies: @@ -35324,13 +35966,13 @@ snapshots: mdurl@2.0.0: {} - mdx-bundler@10.0.2(esbuild@0.25.2)(supports-color@8.1.1): + mdx-bundler@10.0.2(esbuild@0.25.12)(supports-color@8.1.1): dependencies: '@babel/runtime': 7.26.10 - '@esbuild-plugins/node-resolve': 0.2.2(esbuild@0.25.2)(supports-color@8.1.1) + '@esbuild-plugins/node-resolve': 0.2.2(esbuild@0.25.12)(supports-color@8.1.1) '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@mdx-js/esbuild': 3.0.1(esbuild@0.25.2)(supports-color@8.1.1) - esbuild: 0.25.2 + '@mdx-js/esbuild': 3.0.1(esbuild@0.25.12)(supports-color@8.1.1) + esbuild: 0.25.12 gray-matter: 4.0.3 remark-frontmatter: 5.0.0(supports-color@8.1.1) remark-mdx-frontmatter: 4.0.0 @@ -36173,6 +36815,32 @@ snapshots: transitivePeerDependencies: - '@types/node' + msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 5.1.8(@types/node@25.1.0) + '@mswjs/interceptors': 0.39.7 + '@open-draft/deferred-promise': 2.2.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + graphql: 16.11.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.30.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/node' + muggle-string@0.4.1: {} mustache@4.2.0: {} @@ -36226,11 +36894,11 @@ snapshots: neo-async@2.6.2: {} - next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1): + next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.12)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1): dependencies: - '@contentlayer2/core': 0.4.3(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + '@contentlayer2/core': 0.4.3(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 - contentlayer2: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + contentlayer2: 0.4.6(esbuild@0.25.12)(markdown-wasm@1.2.0)(supports-color@8.1.1) next: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -36332,7 +37000,7 @@ snapshots: nice-try@1.0.5: {} - nitropack@2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1): + nitropack@2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.50.2) @@ -36353,7 +37021,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)) + db0: 0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)) defu: 6.1.4 destr: 2.0.5 dot-prop: 9.0.0 @@ -36399,7 +37067,7 @@ snapshots: unenv: 2.0.0-rc.21 unimport: 5.2.0 unplugin-utils: 0.3.0 - unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) + unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.11 @@ -36530,6 +37198,8 @@ snapshots: node-releases@2.0.21: {} + node-releases@2.0.27: {} + node-sql-parser@4.18.0: dependencies: big-integer: 1.6.51 @@ -36666,15 +37336,15 @@ snapshots: next: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nuxt@4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@25.1.0)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 2.6.4(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@nuxt/devtools': 2.6.4(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@nuxt/kit': 4.1.2(magicast@0.3.5) '@nuxt/schema': 4.1.2 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.1.2(@types/node@22.13.14)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) + '@nuxt/vite-builder': 4.1.2(@types/node@25.1.0)(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) '@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2)) '@vue/shared': 3.5.21 c12: 3.3.0(magicast@0.3.5) @@ -36701,7 +37371,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) + nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) nypm: 0.6.2 ofetch: 1.4.1 ohash: 2.0.11 @@ -36725,7 +37395,7 @@ snapshots: unimport: 5.2.0 unplugin: 2.3.10 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.21)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)) - unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) + unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) untyped: 2.0.0 vue: 3.5.21(typescript@5.9.2) vue-bundle-renderer: 2.1.2 @@ -36733,7 +37403,7 @@ snapshots: vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2)) optionalDependencies: '@parcel/watcher': 2.5.1 - '@types/node': 22.13.14 + '@types/node': 25.1.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -36789,7 +37459,7 @@ snapshots: - xml2js - yaml - nwsapi@2.2.20: + nwsapi@2.2.23: optional: true nypm@0.6.2: @@ -37009,6 +37679,15 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -37216,9 +37895,9 @@ snapshots: dependencies: entities: 4.5.0 - parse5@7.2.1: + parse5@7.3.0: dependencies: - entities: 4.5.0 + entities: 6.0.1 optional: true parseurl@1.3.3: {} @@ -37340,7 +38019,7 @@ snapshots: periscopic@4.0.2: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.8 is-reference: 3.0.3 zimmerframe: 1.1.4 optional: true @@ -37638,6 +38317,14 @@ snapshots: postcss: 8.5.6 ts-node: 10.9.2(@types/node@22.13.14)(typescript@5.9.2) + postcss-load-config@4.0.1(postcss@8.5.6)(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)): + dependencies: + lilconfig: 2.1.0 + yaml: 2.8.1 + optionalDependencies: + postcss: 8.5.6 + ts-node: 10.9.2(@types/node@25.1.0)(typescript@5.9.2) + postcss-merge-longhand@7.0.5(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -37963,7 +38650,9 @@ snapshots: proxy-from-env@1.1.0: {} - psl@1.9.0: + psl@1.15.0: + dependencies: + punycode: 2.3.1 optional: true pump@3.0.3: @@ -39139,7 +39828,7 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - schema-utils@4.3.0: + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 @@ -39284,7 +39973,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2): + shadcn@3.3.1(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2): dependencies: '@antfu/ni': 25.0.0 '@babel/core': 7.28.4(supports-color@8.1.1) @@ -39305,7 +39994,7 @@ snapshots: fuzzysort: 3.1.0 https-proxy-agent: 7.0.6(supports-color@8.1.1) kleur: 4.1.5 - msw: 2.11.3(@types/node@22.13.14)(typescript@5.9.2) + msw: 2.11.3(@types/node@25.1.0)(typescript@5.9.2) node-fetch: 3.3.2 ora: 8.2.0 postcss: 8.5.6 @@ -40134,8 +40823,37 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.1(postcss@8.5.6)(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) + postcss-nested: 6.0.1(postcss@8.5.6) + postcss-selector-parser: 6.0.13 + resolve: 1.22.10 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + tapable@2.2.1: {} + tapable@2.3.0: {} + tar-stream@3.1.7: dependencies: b4a: 1.6.7 @@ -40156,25 +40874,25 @@ snapshots: termi-link@1.1.0: {} - terser-webpack-plugin@5.3.14(esbuild@0.25.2)(webpack@5.94.0(esbuild@0.25.2)): + terser-webpack-plugin@5.3.16(esbuild@0.25.12)(webpack@5.94.0(esbuild@0.25.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.0 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.94.0(esbuild@0.25.2) + terser: 5.46.0 + webpack: 5.94.0(esbuild@0.25.12) optionalDependencies: - esbuild: 0.25.2 + esbuild: 0.25.12 optional: true - terser-webpack-plugin@5.3.14(webpack@5.94.0): + terser-webpack-plugin@5.3.16(webpack@5.94.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.0 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.39.0 + terser: 5.46.0 webpack: 5.94.0 terser@5.39.0: @@ -40184,6 +40902,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.46.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -40318,7 +41043,7 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.15.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 @@ -40378,7 +41103,7 @@ snapshots: ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 + '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 @@ -40394,6 +41119,25 @@ snapshots: yn: 3.1.1 optional: true + ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 25.1.0 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-pattern@5.1.1: {} tsconfck@3.0.3(typescript@5.9.2): @@ -40423,7 +41167,7 @@ snapshots: tsx@4.19.2: dependencies: - esbuild: 0.25.2 + esbuild: 0.25.12 get-tsconfig: 4.10.0 optionalDependencies: fsevents: 2.3.3 @@ -40596,6 +41340,8 @@ snapshots: undici-types@6.20.0: {} + undici-types@7.16.0: {} + undici@6.23.0: {} undici@7.18.2: {} @@ -40835,7 +41581,7 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - unstorage@1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)): + unstorage@1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -40847,7 +41593,7 @@ snapshots: ufo: 1.6.3 optionalDependencies: aws4fetch: 1.0.20 - db0: 0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)) + db0: 0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)) ioredis: 5.7.0(supports-color@8.1.1) until-async@3.0.2: {} @@ -40881,6 +41627,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + upper-case-first@2.0.2: dependencies: tslib: 2.8.1 @@ -41083,7 +41835,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vinxi@0.5.3(@electric-sql/pglite@0.2.15)(@types/node@22.13.14)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1): + vinxi@0.5.3(@electric-sql/pglite@0.2.15)(@types/node@25.1.0)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@babel/core': 7.28.4(supports-color@8.1.1) '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4(supports-color@8.1.1)) @@ -41098,14 +41850,14 @@ snapshots: dax-sh: 0.39.2 defu: 6.1.4 es-module-lexer: 1.7.0 - esbuild: 0.25.2 + esbuild: 0.25.12 fast-glob: 3.3.3 get-port-please: 3.2.0 h3: 1.15.5 hookable: 5.5.3 http-proxy: 1.18.1 micromatch: 4.0.8 - nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) + nitropack: 2.12.6(@electric-sql/pglite@0.2.15)(aws4fetch@1.0.20)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(encoding@0.1.13)(supports-color@8.1.1) node-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -41116,8 +41868,8 @@ snapshots: ufo: 1.6.3 unctx: 2.4.1 unenv: 1.10.0 - unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + unstorage: 1.17.1(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.7(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(ioredis@5.7.0(supports-color@8.1.1)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -41162,23 +41914,23 @@ snapshots: - xml2js - yaml - vite-dev-rpc@1.1.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-dev-rpc@1.1.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: birpc: 2.5.0 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-hot-client: 2.1.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-hot-client: 2.1.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) - vite-hot-client@2.1.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) - vite-node@3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -41193,7 +41945,28 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.10.3(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-node@3.2.4(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-checker@0.10.3(eslint@9.39.2(jiti@2.5.1)(supports-color@8.1.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -41203,13 +41976,14 @@ snapshots: strip-ansi: 7.1.0 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: - eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint: 9.39.2(jiti@2.5.1)(supports-color@8.1.1) + optionator: 0.9.4 typescript: 5.9.2 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(supports-color@8.1.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(supports-color@8.1.1)(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: ansis: 4.1.0 debug: 4.4.3(supports-color@8.1.1) @@ -41219,35 +41993,35 @@ snapshots: perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.0 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-dev-rpc: 1.1.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-dev-rpc: 1.1.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) optionalDependencies: '@nuxt/kit': 3.19.2(magicast@0.3.5) transitivePeerDependencies: - supports-color - vite-plugin-vue-tracer@1.0.0(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): + vite-plugin-vue-tracer@1.0.0(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): dependencies: estree-walker: 3.0.3 exsolve: 1.0.7 magic-string: 0.30.21 pathe: 2.0.3 source-map-js: 1.2.1 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) - vite-tsconfig-paths@4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-tsconfig-paths@4.3.2(supports-color@8.1.1)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: debug: 4.3.7(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.9.2) optionalDependencies: - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1): + vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: esbuild: 0.25.2 fdir: 6.5.0(picomatch@4.0.3) @@ -41260,20 +42034,37 @@ snapshots: fsevents: 2.3.3 jiti: 2.5.1 sass: 1.77.4 - terser: 5.39.0 + terser: 5.46.0 tsx: 4.20.3 yaml: 2.8.1 - vitefu@1.1.1(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + esbuild: 0.25.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.50.2 + tinyglobby: 0.2.15 optionalDependencies: - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + '@types/node': 25.1.0 + fsevents: 2.3.3 + jiti: 2.5.1 + sass: 1.77.4 + terser: 5.46.0 + tsx: 4.20.3 + yaml: 2.8.1 + + vitefu@1.1.1(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)): + optionalDependencies: + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) optional: true - vitest@3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -41291,8 +42082,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.14 @@ -41312,6 +42103,49 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(vite@7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.3(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.11(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@25.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.1.0 + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 20.0.3(supports-color@8.1.1) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -41363,7 +42197,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - watchpack@2.4.2: + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -41406,6 +42240,8 @@ snapshots: webpack-sources@3.2.3: {} + webpack-sources@3.3.3: {} + webpack-virtual-modules@0.5.0: {} webpack-virtual-modules@0.6.2: {} @@ -41418,29 +42254,29 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) - browserslist: 4.26.2 + browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.4 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(webpack@5.94.0) - watchpack: 2.4.2 - webpack-sources: 3.2.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(webpack@5.94.0) + watchpack: 2.5.1 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.94.0(esbuild@0.25.2): + webpack@5.94.0(esbuild@0.25.12): dependencies: '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.14.1 @@ -41448,23 +42284,23 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) - browserslist: 4.26.2 + browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.4 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(esbuild@0.25.2)(webpack@5.94.0(esbuild@0.25.2)) - watchpack: 2.4.2 - webpack-sources: 3.2.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(esbuild@0.25.12)(webpack@5.94.0(esbuild@0.25.12)) + watchpack: 2.5.1 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild @@ -41570,6 +42406,8 @@ snapshots: dependencies: string-width: 7.2.0 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -41607,6 +42445,9 @@ snapshots: ws@8.18.3: {} + ws@8.19.0: + optional: true + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 @@ -41675,7 +42516,7 @@ snapshots: yjs@13.6.27: dependencies: - lib0: 0.2.108 + lib0: 0.2.117 yn@3.1.1: optional: true From 28443251ffcec2f4346013a3a2fa339ab631e09b Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 6 Feb 2026 13:59:45 -0700 Subject: [PATCH 07/22] fix: updated deployments with unbroken pnpm (#42570) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Update lock file changes ## Summary by CodeRabbit * **Refactor** * Reorganized internal imports and improved type compatibility handling for the AI documentation API to ensure robust operation across different OpenAI client versions. --- apps/docs/app/api/ai/docs/route.ts | 8 +-- pnpm-lock.yaml | 80 +++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/apps/docs/app/api/ai/docs/route.ts b/apps/docs/app/api/ai/docs/route.ts index 8e3df4206a6..d8c5b49c1ec 100644 --- a/apps/docs/app/api/ai/docs/route.ts +++ b/apps/docs/app/api/ai/docs/route.ts @@ -1,10 +1,9 @@ import { SupabaseClient } from '@supabase/supabase-js' -import { ApplicationError, UserError, clippy } from 'ai-commands/edge' +import { ApplicationError, clippy, UserError } from 'ai-commands/edge' +import { isFeatureEnabled } from 'common/enabled-features' import { NextRequest, NextResponse } from 'next/server' import OpenAI from 'openai' -import { isFeatureEnabled } from 'common/enabled-features' - export const runtime = 'edge' /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that overlap with the OpenAI API regions. @@ -57,7 +56,8 @@ export async function POST(req: NextRequest) { } const useAltSearchIndex = !isFeatureEnabled('search:fullIndex') - const response = await clippy(openai, supabaseClient, messages, { + // @ts-ignore: We have 6 version of openai and the types are not compatible with each other, we need to consolidate to one + const response = await clippy(openai as any, supabaseClient, messages, { useAltSearchIndex, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e94fb5640d..32a96ed16e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,7 +359,7 @@ importers: version: 1.6.0 '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) + version: 0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2))) '@types/lodash.template': specifier: 4.5.0 version: 4.5.0 @@ -389,7 +389,7 @@ importers: version: 1.6.0 tailwindcss: specifier: 'catalog:' - version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + version: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -924,7 +924,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^5.9.0 - version: 5.9.0(ws@8.18.3)(zod@3.25.76) + version: 5.9.0(ws@8.19.0)(zod@3.25.76) openapi-fetch: specifier: 0.12.4 version: 0.12.4 @@ -1306,7 +1306,7 @@ importers: version: 2.7.1(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@16.0.11(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) openai: specifier: ^4.75.1 - version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) openapi-fetch: specifier: 0.12.4 version: 0.12.4 @@ -1535,7 +1535,7 @@ importers: version: link:../../packages/api-types autoevals: specifier: ^0.0.131 - version: 0.0.131(encoding@0.1.13)(ws@8.18.3) + version: 0.0.131(encoding@0.1.13)(ws@8.19.0) braintrust: specifier: ^1.0.2 version: 1.0.2(@aws-sdk/credential-provider-web-identity@3.830.0)(supports-color@8.1.1)(zod@3.25.76) @@ -1748,7 +1748,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^5.9.0 - version: 5.9.0(ws@8.18.3)(zod@3.25.76) + version: 5.9.0(ws@8.19.0)(zod@3.25.76) openapi-fetch: specifier: 0.12.4 version: 0.12.4 @@ -2031,7 +2031,7 @@ importers: version: 2.8.1(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) openai: specifier: ^4.75.1 - version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) parse-numeric-range: specifier: ^1.3.0 version: 1.3.0 @@ -2152,7 +2152,7 @@ importers: version: 7.47.0(react@18.3.1) tailwindcss: specifier: ^3.4.1 - version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + version: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -2265,7 +2265,7 @@ importers: version: 3.5.0 openai: specifier: ^4.75.1 - version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) zod: specifier: 'catalog:' version: 3.25.76 @@ -2429,10 +2429,10 @@ importers: version: 0.1.9 '@tailwindcss/forms': specifier: ^0.5.0 - version: 0.5.6(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) + version: 0.5.6(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2))) '@tailwindcss/typography': specifier: ^0.5.9 - version: 0.5.10(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) + version: 0.5.10(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2))) deepmerge: specifier: ^4.2.2 version: 4.3.1 @@ -2445,10 +2445,10 @@ importers: devDependencies: tailwindcss: specifier: 'catalog:' - version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + version: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tailwindcss-animate: specifier: ^1.0.6 - version: 1.0.7(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) + version: 1.0.7(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2))) typescript: specifier: 'catalog:' version: 5.9.2 @@ -2497,7 +2497,7 @@ importers: version: 5.9.2 vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) packages/eslint-config-supabase: devDependencies: @@ -2929,7 +2929,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^4.75.1 - version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) react: specifier: 'catalog:' version: 18.3.1 @@ -28549,11 +28549,20 @@ snapshots: dependencies: tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)))': + dependencies: + tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) + '@tailwindcss/forms@0.5.6(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)))': dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + '@tailwindcss/forms@0.5.6(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)))': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) + '@tailwindcss/typography@0.5.10(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)))': dependencies: lodash.castarray: 4.4.0 @@ -28562,6 +28571,14 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + '@tailwindcss/typography@0.5.10(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)))': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) + '@tanstack/directive-functions-plugin@1.114.12(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1)': dependencies: '@babel/code-frame': 7.26.2 @@ -29969,7 +29986,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -30028,7 +30045,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@25.1.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@25.1.0)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -30659,7 +30676,7 @@ snapshots: auto-bind@4.0.0: {} - autoevals@0.0.131(encoding@0.1.13)(ws@8.18.3): + autoevals@0.0.131(encoding@0.1.13)(ws@8.19.0): dependencies: ajv: 8.17.1 compute-cosine-similarity: 1.1.0 @@ -30667,7 +30684,7 @@ snapshots: js-yaml: 4.1.1 linear-sum-assignment: 1.0.9 mustache: 4.2.0 - openai: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + openai: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) zod: 3.25.76 zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: @@ -37627,9 +37644,24 @@ snapshots: transitivePeerDependencies: - encoding - openai@5.9.0(ws@8.18.3)(zod@3.25.76): + openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76): + dependencies: + '@types/node': 18.18.13 + '@types/node-fetch': 2.6.6 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) optionalDependencies: - ws: 8.18.3 + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + + openai@5.9.0(ws@8.19.0)(zod@3.25.76): + optionalDependencies: + ws: 8.19.0 zod: 3.25.76 openapi-fetch@0.12.4: @@ -38019,7 +38051,7 @@ snapshots: periscopic@4.0.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.5 is-reference: 3.0.3 zimmerframe: 1.1.4 optional: true @@ -40790,9 +40822,9 @@ snapshots: tailwind-merge@3.3.1: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2))): dependencies: - tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + tailwindcss: 3.4.1(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.2)) tailwindcss-radix@2.8.0: {} From 32817d2e74360db36ad56484b197791eda87fcb6 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Sat, 7 Feb 2026 15:24:07 -0700 Subject: [PATCH 08/22] fix: centered messages table editor (#42571) --- apps/studio/components/grid/components/grid/Grid.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx index 12fe5bc307e..4b7890caeae 100644 --- a/apps/studio/components/grid/components/grid/Grid.tsx +++ b/apps/studio/components/grid/components/grid/Grid.tsx @@ -237,7 +237,7 @@ export const Grid = memo( {(rows ?? []).length === 0 && (
{page > 1 ? ( -
+

This page does not have any data

) : (filters ?? []).length === 0 ? ( -
+

{isDraggedOver ? ( isValidFileDraggedOver ? ( @@ -312,7 +312,7 @@ export const Grid = memo( )}

) : ( -
+

The filters applied have returned no results from this table

From e8bb51ec84f4c09aae90eda8b59da477f0b1c4e3 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:25:06 +1100 Subject: [PATCH 09/22] chore(studio): queue operations UI improvements (#42272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? UI improvements ## What is the current behavior? @awaseem added a schmick new “Queue table operations” feature in https://github.com/supabase/supabase/pull/42120 ## What is the new behavior? This adds some UI polish to that feature. ## Additional context https://github.com/user-attachments/assets/0b823bc9-44bd-42d1-8042-162084d058c7 ## Summary by CodeRabbit * **UI/UX Improvements** * Repositioned and animated floating save/review bar for smoother entrance and centered alignment * Card-based redesign of operation items with clearer old/new value badges and improved action controls * Updated side panel text, button labels ("Review", "Save/Revert" with "all" when applicable), and focus behavior * Refined empty-state layout and amber background highlighting for modified cells * Improved spacing, padding, and visual polish across the operation queue UI ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Ali Waseem --- .../footer/operations/SaveQueueActionBar.tsx | 104 ++++++++++++------ .../AddRowOperationItem.tsx | 55 +++++---- .../DeleteRowOperationItem.tsx | 53 +++++---- .../OperationQueueSidePanel/OperationItem.tsx | 53 +++++---- .../OperationQueueSidePanel/OperationList.tsx | 22 ++-- .../OperationQueueSidePanel.tsx | 19 ++-- apps/studio/styles/grid.scss | 26 ++--- .../features/queue-table-operations.spec.ts | 54 ++++----- 8 files changed, 231 insertions(+), 155 deletions(-) diff --git a/apps/studio/components/grid/components/footer/operations/SaveQueueActionBar.tsx b/apps/studio/components/grid/components/footer/operations/SaveQueueActionBar.tsx index c554f2065dc..8975417d268 100644 --- a/apps/studio/components/grid/components/footer/operations/SaveQueueActionBar.tsx +++ b/apps/studio/components/grid/components/footer/operations/SaveQueueActionBar.tsx @@ -2,11 +2,10 @@ import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueu import { useOperationQueueShortcuts } from 'components/grid/hooks/useOperationQueueShortcuts' import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { AnimatePresence, motion } from 'framer-motion' -import { Eye } from 'lucide-react' import { createPortal } from 'react-dom' import { useTableEditorStateSnapshot } from 'state/table-editor' import { Button } from 'ui' - +import { useEffect, useRef, useState } from 'react' import { getModKeyLabel } from '@/lib/helpers' export const SaveQueueActionBar = () => { @@ -14,6 +13,7 @@ export const SaveQueueActionBar = () => { const snap = useTableEditorStateSnapshot() const isQueueOperationsEnabled = useIsQueueOperationsEnabled() const { handleSave } = useOperationQueueActions() + const [leftPosition, setLeftPosition] = useState('50%') useOperationQueueShortcuts() @@ -24,42 +24,80 @@ export const SaveQueueActionBar = () => { const isVisible = isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen + // Center position relative to grid container (viewport alignment) + useEffect(() => { + if (!isVisible) return + + const gridContainer = document.querySelector('.sb-grid') + const updatePosition = () => { + if (!gridContainer) { + setLeftPosition('50%') + return + } + + const gridRect = gridContainer.getBoundingClientRect() + const gridCenter = gridRect.left + gridRect.width / 2 + setLeftPosition(`${gridCenter}px`) + } + + updatePosition() + + if (!gridContainer) return + + const resizeObserver = new ResizeObserver(updatePosition) + resizeObserver.observe(gridContainer) + + window.addEventListener('resize', updatePosition) + window.addEventListener('scroll', updatePosition, true) + + return () => { + resizeObserver.disconnect() + window.removeEventListener('resize', updatePosition) + window.removeEventListener('scroll', updatePosition, true) + } + }, [isVisible]) + const content = ( {isVisible && ( - -
- - {operationCount} pending change{operationCount !== 1 ? 's' : ''} - -
- - + +
+ + {operationCount} pending change{operationCount !== 1 ? 's' : ''} + +
+ + +
-
- + +
)}
) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx index 6dcd99ce3e0..0b2878cee77 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx @@ -1,12 +1,13 @@ import { useQueryClient } from '@tanstack/react-query' +import { Plus, Undo2 } from 'lucide-react' import { tableRowKeys } from 'data/table-rows/keys' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Plus, X } from 'lucide-react' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { Button } from 'ui' import { formatOperationItemValue } from './OperationQueueSidePanel.utils' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { AddRowPayload } from '@/state/table-editor-operation-queue.types' +import { Card, CardContent, CardHeader } from 'ui' interface AddRowOperationItemProps { operationId: string @@ -47,40 +48,52 @@ export const AddRowOperationItem = ({ } return ( -
-
+ +
- -
-
{fullTableName}
-
- New row +
+ + {fullTableName} + +
+ New row
-
+ -
+ {previewColumns.map(([key, value]) => ( -
- {key}: - +
+ + + {key}: + {formatOperationItemValue(value)}
))} {remainingCount > 0 && ( -
+{remainingCount} more column(s)
+
+ + + +{remainingCount} more column(s) +
)} -
-
+ + ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx index 8e641460788..56069c3b2c6 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx @@ -1,12 +1,13 @@ import { useQueryClient } from '@tanstack/react-query' -import { Trash2, X } from 'lucide-react' -import { Button } from 'ui' - +import { Undo2 } from 'lucide-react' import { tableRowKeys } from 'data/table-rows/keys' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types' + import { formatOperationItemValue } from './OperationQueueSidePanel.utils' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types' +import { Card, CardContent, CardHeader } from 'ui' interface DeleteRowOperationItemProps { operationId: string @@ -45,32 +46,40 @@ export const DeleteRowOperationItem = ({ } return ( -
-
+ +
- -
-
{fullTableName}
-
- Delete row - · - where {whereClause} +
+ + {fullTableName} + +
+ Delete row + · + where {whereClause}
-
+ -
-
Row will be deleted
-
-
+ +
Row will be deleted
+
+
) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx index c9476493de6..4773099a270 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx @@ -1,12 +1,13 @@ import { useQueryClient } from '@tanstack/react-query' +import { Undo2 } from 'lucide-react' import { tableRowKeys } from 'data/table-rows/keys' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { X } from 'lucide-react' import { useTableEditorStateSnapshot } from 'state/table-editor' import { formatOperationItemValue } from './OperationQueueSidePanel.utils' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { EditCellContentPayload } from '@/state/table-editor-operation-queue.types' +import { Card, CardContent, CardHeader } from 'ui' interface OperationItemProps { operationId: string @@ -44,42 +45,50 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr } return ( -
-
+ +
-
{fullTableName}
-
- {columnName} - • - where {whereClause} + + {fullTableName} + +
+ {columnName} + · + where {whereClause}
} + aria-label="Revert change" + className="px-1.5" + icon={} onClick={handleDelete} - className="shrink-0 w-7" - aria-label="Remove operation" - tooltip={{ content: { side: 'bottom', text: 'Remove operation' } }} + tooltip={{ + content: { + side: 'bottom', + align: 'end', + text: 'Revert change', + }, + }} /> -
+
-
-
- - - + +
+ - + {formattedOldValue}
-
- + - +
+ + + {formattedNewValue}
-
-
+ + ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx index 0468be02806..1c3dc3b7559 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx @@ -15,7 +15,11 @@ interface OperationListProps { export const OperationList = ({ operations }: OperationListProps) => { if (operations.length === 0) { - return

No pending changes

+ return ( +
+

No pending changes

+
+ ) } const addOperations = operations.filter(isAddRowOperation) @@ -23,12 +27,13 @@ export const OperationList = ({ operations }: OperationListProps) => { const editOperations = operations.filter(isEditCellContentOperation) return ( -
+
{deleteOperations.length > 0 && (
-

- Rows to Delete ({deleteOperations.length}) +

+ {deleteOperations.length} row deletion{deleteOperations.length !== 1 ? 's' : ''}

+
{deleteOperations.map((op) => ( { {addOperations.length > 0 && (
-

- Rows to Add ({addOperations.length}) +

+ {addOperations.length} row addition{addOperations.length !== 1 ? 's' : ''}

{addOperations.map((op) => ( @@ -62,9 +67,10 @@ export const OperationList = ({ operations }: OperationListProps) => { {editOperations.length > 0 && (
-

- Cell Edits ({editOperations.length}) +

+ {editOperations.length} cell edit{editOperations.length !== 1 ? 's' : ''}

+
{editOperations.map((op) => ( event.preventDefault()} // Prevent focus on first focussable element since it is the revert changes ButtonTooltip header={
Pending changes - + {operations.length} operation{operations.length !== 1 ? 's' : ''}
@@ -41,25 +42,29 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
-
} > - + diff --git a/apps/studio/styles/grid.scss b/apps/studio/styles/grid.scss index 8664d99a400..faed1baaf7c 100644 --- a/apps/studio/styles/grid.scss +++ b/apps/studio/styles/grid.scss @@ -36,17 +36,13 @@ box-shadow: inset 0 0 0 1px #24b47e; } -// Cell with unsaved changes - yellow/amber border +// Cell with unsaved changes - warning (amber) background/text color .rdg-cell.rdg-cell--dirty { - box-shadow: inset 0 0 0 2px hsl(var(--warning-default)); + background-color: hsl(var(--warning-300) / 0.75); + color: hsl(var(--warning-default)); } -// When a dirty cell is also selected, keep the amber border -.rdg-cell.rdg-cell--dirty[aria-selected='true'] { - box-shadow: inset 0 0 0 2px hsl(var(--warning-default)); -} - -// Row pending addition - green background +// Row pending addition - green background, green text .rdg-row.rdg-row--added { background-color: hsl(var(--brand-200) / 0.3); @@ -56,32 +52,33 @@ .rdg-cell { border-left-color: hsl(var(--brand-default)); + color: hsl(var(--brand-link)); } // First cell gets a stronger left border to indicate new row .rdg-cell:first-child { - box-shadow: inset 3px 0 0 0 hsl(var(--brand-default)); + box-shadow: inset 2px 0 0 0 hsl(var(--brand-default)); } } -// Row pending deletion - red background with strikethrough effect +// Row pending deletion - red background with strikethrough effect, red text .rdg-row.rdg-row--deleted { - background-color: hsl(var(--destructive-200) / 0.3); - opacity: 0.6; + background-color: hsl(var(--destructive-300) / 0.3); &:hover { - background-color: hsl(var(--destructive-200) / 0.5); + background-color: hsl(var(--destructive-300) / 0.5); } .rdg-cell { text-decoration: line-through; text-decoration-color: hsl(var(--destructive-default)); border-left-color: hsl(var(--destructive-default)); + color: hsl(var(--destructive-default)); } // First cell gets a stronger left border to indicate deleted row .rdg-cell:first-child { - box-shadow: inset 3px 0 0 0 hsl(var(--destructive-default)); + box-shadow: inset 2px 0 0 0 hsl(var(--destructive-default)); } } @@ -89,7 +86,6 @@ @apply box-border select-none overflow-x-auto overflow-y-scroll bg-dash-canvas; @apply border-t border-r-0 border-l-0; contain: strict; - -webkit-user-select: none; font-size: 14px; } diff --git a/e2e/studio/features/queue-table-operations.spec.ts b/e2e/studio/features/queue-table-operations.spec.ts index 89d637f4e8a..c3eacb6ba71 100644 --- a/e2e/studio/features/queue-table-operations.spec.ts +++ b/e2e/studio/features/queue-table-operations.spec.ts @@ -86,15 +86,15 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('1 pending change')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('Pending changes')).toBeVisible() - await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible() + await expect(sidePanel.getByText('1 cell edit')).toBeVisible() await expect(sidePanel.getByTitle('original value')).toBeVisible() await expect(sidePanel.getByTitle('edited value')).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'edited value' })).toBeVisible() @@ -126,9 +126,9 @@ test.describe('Queue Table Operations', () => { await page.keyboard.press('Enter') await expect(page.getByText('1 pending change')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() - await page.getByRole('button', { name: 'Cancel All' }).click() + await page.getByRole('button', { name: 'Revert', exact: true }).click() await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible() await expect(page.getByRole('gridcell', { name: 'should be cancelled' })).not.toBeVisible() @@ -161,14 +161,14 @@ test.describe('Queue Table Operations', () => { await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('Pending changes')).toBeVisible() - await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible() + await expect(sidePanel.getByText('1 row addition')).toBeVisible() await expect(sidePanel.getByText('New row', { exact: true })).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible() @@ -202,13 +202,13 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('2 pending changes')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('2 operations')).toBeVisible() - await expect(sidePanel.getByText('Rows to Add (2)')).toBeVisible() + await expect(sidePanel.getByText('2 row additions')).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'row one' })).toBeVisible() @@ -243,15 +243,15 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('2 pending changes')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') - const removeButtons = sidePanel.getByRole('button', { name: 'Remove operation' }) + const removeButtons = sidePanel.getByRole('button', { name: 'Revert change' }) await removeButtons.last().click() await expect(sidePanel.getByText('1 operation')).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'keep this row' })).toBeVisible() @@ -285,7 +285,7 @@ test.describe('Queue Table Operations', () => { await expect(page.getByRole('dialog').getByText('Pending changes')).toBeVisible() await page.keyboard.press('ControlOrMeta+.') - await expect(page.getByRole('button', { name: 'View Details' })).toBeVisible() + await expect(page.getByRole('button', { name: /Review/ })).toBeVisible() await page.keyboard.press('ControlOrMeta+s') await expect(page.getByText('Changes saved successfully')).toBeVisible() @@ -318,14 +318,14 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('1 pending change')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('Pending changes')).toBeVisible() - await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible() + await expect(sidePanel.getByText('1 row deletion')).toBeVisible() await expect(sidePanel.getByText('Delete row', { exact: true })).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible() @@ -354,8 +354,8 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('1 pending change')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() - await page.getByRole('button', { name: 'Cancel All' }).click() + await page.getByRole('button', { name: /Review/ }).click() + await page.getByRole('button', { name: 'Revert', exact: true }).click() await expect(page.getByRole('gridcell', { name: 'should not be deleted' })).toBeVisible() await expect(page.getByText('pending change')).not.toBeVisible() @@ -397,14 +397,14 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('3 pending changes')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('3 operations')).toBeVisible() - await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible() - await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible() - await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible() + await expect(sidePanel.getByText('1 row deletion')).toBeVisible() + await expect(sidePanel.getByText('1 row addition')).toBeVisible() + await expect(sidePanel.getByText('1 cell edit')).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await expect(page.getByRole('gridcell', { name: 'edited row' })).toBeVisible() @@ -447,11 +447,11 @@ test.describe('Queue Table Operations', () => { await expect(page.getByText('2 pending changes')).toBeVisible() - await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: /Review/ }).click() const sidePanel = page.getByRole('dialog') await expect(sidePanel.getByText('2 operations')).toBeVisible() - await sidePanel.getByRole('button', { name: /Save All/ }).click() + await sidePanel.getByRole('button', { name: /^Save/ }).click() await expect(page.getByText('Changes saved successfully')).toBeVisible() await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click() From a6541d725e5c61d66bd317b1e1be47e8ab547f10 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Feb 2026 10:43:29 +0800 Subject: [PATCH 10/22] Update cron job overview to opt to skip JOIN depending on query cost (#42492) ## Context The Integrations -> Cron overview page renders a list of cron jobs by doing a join on both `cron.jobs` and `cron.job_run_details` in order to pull the data for "Latest run" and "Status" ("Next run" is just derived from "Latest Run" and the job's schedule) In the past, we've had users run into issues where this query would time out, likely due to the join on `job_run_details` especially if that table is really big (and the columns on that table aren't indexed), and our solution was to inform users to clear out that table + schedule a job to clean the `job_run_details` table on a certain cadence. This CTA however blocks the UI from rendering the cron jobs overview entirely image ## Changes involved Am opting to use the new preflight check introduced [here](https://github.com/supabase/supabase/pulls?q=is%3Apr+author%3Ajoshenlim+is%3Aclosed) - Attempt to fetch the data for the cron jobs overview normally - If preflight check rejects (due to high cost), then opt to fetch the data while omitting the join - Show a banner instead to inform the user accordingly - So that way users can still view their cron jobs (the latest run column is a nice to have convenience really) image - Clicking learn more will then open a dialog to explain what this is all about - The steps to clear the `job_run_details` table + schedule a clean up job still remains image Screenshot 2026-02-05 at 12 56 39 Screenshot 2026-02-05 at 13 01 37 - Non-UI related changes include - Automatically refresh cron jobs after scheduling a job, remove manual CTA to refresh the jobs after scheduling the clean up job ## Summary by CodeRabbit * **New Features** * Previous Runs tab on cron job pages * Interactive "Learn more" cleanup dialog with guided delete and schedule steps * Close-confirmation modal when editing cron jobs * High-cost banner that enables a lightweight "minimal" list mode * **Bug Fixes** * Clearer placeholder (minus icon/message) when a job has no runs * **Performance** * Safer, more efficient fetching and pagination for large cron job lists; reduced retries on high-cost queries * **Tests** * Expanded end-to-end tests covering high-cost flows and the cleanup dialog --- .../Integrations/CronJobs/CronJobPage.tsx | 15 +- .../CronJobs/CronJobTableCell.tsx | 7 +- .../CronJobs/CronJobsTab.CleanupNotice.tsx | 290 ++++++++++-------- .../CronJobs/CronJobsTab.DataGrid.tsx | 10 +- .../Integrations/CronJobs/CronJobsTab.tsx | 97 +----- .../CronJobs/CronJobsTab.useCleanupActions.ts | 55 ++-- .../CronJobs/CronJobsTab.useCronJobsData.ts | 216 +++---------- ...ase-cron-job-run-details-estimate-query.ts | 67 ---- .../database-cron-jobs-infinite-query.ts | 59 +--- ...tabase-cron-jobs-minimal-infinite-query.ts | 67 ++++ apps/studio/data/database-cron-jobs/keys.ts | 2 + .../schedule-clean-up-mutation.ts | 66 ++++ apps/studio/data/sql/queries/get-cron-jobs.ts | 72 +++++ .../hooks/misc/useCronJobsEstimatePrefetch.ts | 29 -- .../[id]/[pageId]/[childId]/index.tsx | 8 +- .../integrations/[id]/[pageId]/index.tsx | 10 +- e2e/studio/features/cron-jobs.spec.ts | 185 +++++++++-- 17 files changed, 644 insertions(+), 611 deletions(-) delete mode 100644 apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts create mode 100644 apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts create mode 100644 apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts create mode 100644 apps/studio/data/sql/queries/get-cron-jobs.ts delete mode 100644 apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx index 20bd50c3d0f..11f05811907 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobPage.tsx @@ -1,14 +1,13 @@ +import { useParams } from 'common' import { toString as CronToString } from 'cronstrue' +import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query' +import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { type ConfirmOnCloseModalProps, useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { Edit3, List } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' - -import { useParams } from 'common' -import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query' -import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' import { BreadcrumbItem_Shadcn_ as BreadcrumbItem, BreadcrumbLink_Shadcn_ as BreadcrumbLink, @@ -16,13 +15,13 @@ import { BreadcrumbPage_Shadcn_ as BreadcrumbPage, BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator, Button, - cn, CodeBlock, Sheet, SheetContent, Tooltip, TooltipContent, TooltipTrigger, + cn, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { @@ -35,6 +34,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' + import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet' import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils' import { PreviousRunsTab } from './PreviousRunsTab' @@ -189,6 +189,7 @@ export const CronJobPage = () => { {secondaryActions.length > 0 && {secondaryActions}} + diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx index 8883ad7c2db..77b488e4a19 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx @@ -1,6 +1,6 @@ import parser from 'cron-parser' import dayjs from 'dayjs' -import { Copy, Edit, MoreVertical, Play, Trash } from 'lucide-react' +import { Copy, Edit, Minus, MoreVertical, Play, Trash } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { useState } from 'react' import { toast } from 'sonner' @@ -90,6 +90,7 @@ export const CronJobTableCell = ({ const [showToggleModal, setShowToggleModal] = useState(false) const value = row?.[col.id] + const hasValue = col.id in row const { jobid, schedule, latest_run, status, active, jobname } = row const formattedValue = @@ -241,7 +242,9 @@ export const CronJobTableCell = ({
{['latest_run', 'next_run'].includes(col.id) ? ( - col.id === 'latest_run' && formattedValue === null ? ( + !hasValue ? ( + + ) : col.id === 'latest_run' && formattedValue === null ? (

Job has not been run yet

) : col.id === 'next_run' && !formattedValue ? (

Unable to parse next run for job

diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx index 5f2f8efdf77..cb31fc6d2e0 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx @@ -1,8 +1,15 @@ -import { CheckCircle2, RefreshCw, XCircle } from 'lucide-react' - +import { getScheduleDeleteCronJobRunDetailsSql } from 'data/sql/queries/delete-cron-job-run-details' +import { CheckCircle2, XCircle } from 'lucide-react' import { Button, CodeBlock, + Dialog, + DialogContent, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, Progress, Select_Shadcn_, SelectContent_Shadcn_, @@ -12,138 +19,178 @@ import { } from 'ui' import { Admonition } from 'ui-patterns/admonition' -import { getScheduleDeleteCronJobRunDetailsSql } from 'data/sql/queries/delete-cron-job-run-details' import { CLEANUP_INTERVALS } from './CronJobsTab.constants' -import type { BatchDeletionProgress, CleanupState } from './CronJobsTab.useCleanupActions' +import { + useCronJobsCleanupActions, + type BatchDeletionProgress, +} from './CronJobsTab.useCleanupActions' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -export interface CronJobRunDetailsOverflowNoticeProps { - estimatedRows?: number - mode: 'confirmed' | 'suspected' - cleanupState: CleanupState - selectedInterval: string - onIntervalChange: (interval: string) => void - onRunDeleteSql: () => void - onRunScheduleSql: () => void - onCancelDeletion: () => void - onRetryDeletion: () => void - onRefresh: () => void +interface CronJobRunDetailsOverflowNoticeV2Props { + refetchJobs: () => void } -export const CronJobRunDetailsOverflowNotice = ({ - estimatedRows, - mode, - cleanupState, - selectedInterval, - onIntervalChange, - onRunDeleteSql, - onRunScheduleSql, - onCancelDeletion, - onRetryDeletion, - onRefresh, -}: CronJobRunDetailsOverflowNoticeProps) => { - const formattedRowEstimate = - typeof estimatedRows === 'number' ? estimatedRows.toLocaleString() : 'unknown' - const noticeTitle = - mode === 'confirmed' - ? 'cron.job_run_details is too large to load' - : 'Cron job overview timed out' - const noticeDescription = - mode === 'confirmed' - ? `We detected approximately ${formattedRowEstimate} rows in cron.job_run_details, which prevents the overview from running.` - : `Loading the cron job overview timed out. The issue might be caused by your cron.job_run_details table having too many rows.` +export const CronJobRunDetailsOverflowNoticeV2 = ( + props: CronJobRunDetailsOverflowNoticeV2Props +) => { + return ( + } + > +

Last run for each cron job omitted due to high query cost

+
+ ) +} + +const CronJobRunDetailsOverflowDialog = ({ + refetchJobs, +}: CronJobRunDetailsOverflowNoticeV2Props) => { + const { data: project } = useSelectedProjectQuery() + + const { + cleanupInterval, + cleanupState, + isScheduling, + isScheduleSuccess, + setCleanupInterval, + runBatchedDeletion, + scheduleCleanup, + cancelDeletion, + } = useCronJobsCleanupActions({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const isDeleting = cleanupState.status === 'deleting' - const isScheduling = cleanupState.status === 'scheduling' const isDeleteSuccess = cleanupState.status === 'delete-success' const isDeleteError = cleanupState.status === 'delete-error' - const isScheduleSuccess = cleanupState.status === 'schedule-success' const isBusy = isDeleting || isScheduling - const canSchedule = isDeleteSuccess || isScheduleSuccess return ( - -
-

- Remove old run history now, then schedule a cron job that keeps trimming{' '} - cron.job_run_details automatically so the overview remains responsive. -

+ + + + + + + Last run for cron jobs omitted for overview + + + +

+ The dashboard fetches data for the cron jobs overview by running a join between the{' '} + cron.job and{' '} + cron.job_run_details tables to + show each cron job's latest run. +

- {/* Step 1: Delete older entries */} -
-

Step 1: Delete older entries

+

+ However, the join was skipped as the estimated query cost exceeds safety thresholds, + likely due to the size of{' '} + cron.job_run_details table. +

+ - {isDeleting ? ( - - ) : isDeleteSuccess ? ( - - ) : isDeleteError ? ( - - ) : ( -
-
- - - - - - {CLEANUP_INTERVALS.map((option) => ( - - {option.label} - - ))} - - -
- -
- )} -
+ - {/* Step 2: Schedule automated cleanup (only available after successful deletion) */} -
-

Step 2: Schedule an automated cleanup

+ +

+ Suggested steps +

- {!canSchedule ? ( -

- Complete step 1 to enable scheduling a daily cleanup job. -

- ) : isScheduleSuccess ? ( - - ) : ( - <> - + We recommend removing the old run history now, then scheduling a cron job that keeps + trimming the cron.job_run_details table + automatically. This also prevents unnecessary bloat on the database. +

+ +
+

Step 1: Delete older entries

+ + {isDeleting ? ( + + ) : isDeleteSuccess ? ( + + ) : isDeleteError ? ( + runBatchedDeletion(cleanupInterval)} /> - - - )} -
-
- + ) : ( +
+
+ + + + + + {CLEANUP_INTERVALS.map((option) => ( + + {option.label} + + ))} + + +
+ +
+ )} +
+ +
+

Step 2: Schedule an automated cleanup

+ + {!canSchedule ? ( +

+ Complete step 1 to enable scheduling a daily cleanup job. +

+ ) : isScheduleSuccess ? ( + + ) : ( + <> + + + + )} +
+ + + ) } @@ -205,11 +252,7 @@ const DeletionError = ({ error, onRetry }: DeletionErrorProps) => (
) -interface ScheduleSuccessProps { - onRefresh: () => void -} - -const ScheduleSuccess = ({ onRefresh }: ScheduleSuccessProps) => ( +const ScheduleSuccess = () => (
@@ -217,9 +260,8 @@ const ScheduleSuccess = ({ onRefresh }: ScheduleSuccessProps) => (

- Refresh to reload the cron jobs and view the new cleanup job. + New cleanup job should now be visible in the cron jobs overview.

-
) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx index b28921c4dfa..2d5063c263d 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx @@ -1,4 +1,4 @@ -import { type MouseEvent, type ReactNode, type UIEvent } from 'react' +import { type MouseEvent, type UIEvent } from 'react' import DataGrid, { type Column, Row } from 'react-data-grid' import AlertError from 'components/ui/AlertError' @@ -15,7 +15,6 @@ interface CronJobsTabDataGridProps { searchQuery?: string | null onScroll: (event: UIEvent) => void onRowClick: (row: CronJob, event: MouseEvent) => void - overlay?: ReactNode } export const CronJobsTabDataGrid = ({ @@ -26,16 +25,13 @@ export const CronJobsTabDataGrid = ({ searchQuery, onScroll, onRowClick, - overlay, }: CronJobsTabDataGridProps) => { - const fallbackContent = overlay ? ( -
{overlay}
- ) : isLoading ? ( + const fallbackContent = isLoading ? (
) : error ? ( -
+
) : ( diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 9ce5ae58730..f893e73db28 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -1,9 +1,3 @@ -import { Loader2 } from 'lucide-react' -import { useRouter } from 'next/router' -import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' -import { MouseEvent, UIEvent, useMemo, useRef, useState } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet' import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' @@ -13,17 +7,21 @@ import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQuer import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers' import { createNavigationHandler } from 'lib/navigation' import { isGreaterThanOrEqual } from 'lib/semver' -import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers' +import { Loader2 } from 'lucide-react' +import { useRouter } from 'next/router' +import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' +import { MouseEvent, UIEvent, useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' import { LoadingLine, Sheet, SheetContent } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' + import { formatCronJobColumns } from './CronJobs.utils' -import { CronJobRunDetailsOverflowNotice } from './CronJobsTab.CleanupNotice' +import { CronJobRunDetailsOverflowNoticeV2 } from './CronJobsTab.CleanupNotice' import { CronJobsTabDataGrid } from './CronJobsTab.DataGrid' -import { CronJobRunDetailsEstimateErrorNotice } from './CronJobsTab.EstimateErrorNotice' import { CronJobsTabHeader } from './CronJobsTab.Header' -import { useCronJobsCleanupActions } from './CronJobsTab.useCleanupActions' import { useCronJobsData } from './CronJobsTab.useCronJobsData' import { DeleteCronJob } from './DeleteCronJob' @@ -47,83 +45,12 @@ export const CronjobsTab = () => { setSearchQuery(null) } - const { dataStatus, isQueryEnabled, grid, count } = useCronJobsData({ + const { grid, count } = useCronJobsData({ projectRef: project?.ref, connectionString: project?.connectionString, searchQuery, }) - const { - cleanupInterval, - setCleanupInterval, - cleanupState, - runBatchedDeletion, - scheduleCleanup, - cancelDeletion, - } = useCronJobsCleanupActions({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const gridOverlay = useMemo(() => { - switch (dataStatus.status) { - case 'overflow-confirmed': - return ( - runBatchedDeletion(cleanupInterval)} - onRunScheduleSql={() => scheduleCleanup(cleanupInterval)} - onCancelDeletion={cancelDeletion} - onRetryDeletion={() => runBatchedDeletion(cleanupInterval)} - onRefresh={grid.refetch} - /> - ) - - case 'overflow-suspected': - return ( - runBatchedDeletion(cleanupInterval)} - onRunScheduleSql={() => scheduleCleanup(cleanupInterval)} - onCancelDeletion={cancelDeletion} - onRetryDeletion={() => runBatchedDeletion(cleanupInterval)} - onRefresh={grid.refetch} - /> - ) - - case 'estimate-error': - return ( - - ) - - case 'loading': - case 'ready': - default: - return undefined - } - }, [ - dataStatus, - cleanupInterval, - setCleanupInterval, - cleanupState, - runBatchedDeletion, - scheduleCleanup, - cancelDeletion, - grid.refetch, - ]) - const deletingCronJobIdRef = useRef(null) const { setValue: setCronJobForEditing, value: cronJobForEditing } = useQueryStateWithSelect({ @@ -184,8 +111,6 @@ export const CronjobsTab = () => { const xScroll = useRef(0) const handleScroll = (event: UIEvent) => { - if (!isQueryEnabled) return - const isScrollingHorizontally = xScroll.current !== event.currentTarget.scrollLeft xScroll.current = event.currentTarget.scrollLeft @@ -259,6 +184,7 @@ export const CronjobsTab = () => { onCreateJob={onOpenCreateJobSheet} /> + {grid.isMinimal && } { searchQuery={searchQuery} onScroll={handleScroll} onRowClick={handleRowClick} - overlay={gridOverlay} />
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCleanupActions.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCleanupActions.ts index 16ea05d0ada..9800370904a 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCleanupActions.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCleanupActions.ts @@ -1,7 +1,3 @@ -import { useCallback, useRef, useState } from 'react' -import { toast } from 'sonner' - -import type { ConnectionVars } from '@/data/common.types' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { CTID_BATCH_PAGE_SIZE, @@ -9,10 +5,13 @@ import { getDeleteOldCronJobRunDetailsByCtidSql, getJobRunDetailsPageCountKey, getJobRunDetailsPageCountSql, - getScheduleDeleteCronJobRunDetailsKey, - getScheduleDeleteCronJobRunDetailsSql, } from 'data/sql/queries/delete-cron-job-run-details' +import { useCallback, useRef, useState } from 'react' +import { toast } from 'sonner' + import { CLEANUP_INTERVALS } from './CronJobsTab.constants' +import type { ConnectionVars } from '@/data/common.types' +import { useScheduleCronJobRunDetailsCleanupMutation } from '@/data/database-cron-jobs/schedule-clean-up-mutation' // Delay between batches to allow other queries to proceed (in milliseconds) const BATCH_DELAY_MS = 100 @@ -30,9 +29,6 @@ export type CleanupState = | { status: 'deleting'; progress: BatchDeletionProgress } | { status: 'delete-success'; totalRowsDeleted: number } | { status: 'delete-error'; error: string } - | { status: 'scheduling' } - | { status: 'schedule-success' } - | { status: 'schedule-error'; error: string } export const useCronJobsCleanupActions = ({ projectRef, @@ -48,6 +44,12 @@ export const useCronJobsCleanupActions = ({ onError: () => {}, // Error handled inline }) + const { + mutate: scheduleCronJobCleanup, + isPending: isScheduling, + isSuccess: isScheduleSuccess, + } = useScheduleCronJobRunDetailsCleanupMutation() + /** * Run batched deletion using ctid ranges. * This approach scans the table in page chunks to avoid: @@ -157,33 +159,24 @@ export const useCronJobsCleanupActions = ({ * This should only be called after a successful initial deletion. */ const scheduleCleanup = useCallback( - async (interval: string) => { + async ({ interval, onSuccess }: { interval: string; onSuccess?: () => void }) => { if (!projectRef) { console.error('[CronJobsTab > schedule cleanup] Project reference is required') toast.error('There was an error scheduling the cleanup. Please try again.') return } - try { - setCleanupState({ status: 'scheduling' }) - - await executeSql({ - projectRef, - connectionString, - sql: getScheduleDeleteCronJobRunDetailsSql(interval), - queryKey: getScheduleDeleteCronJobRunDetailsKey(projectRef, interval), - }) - - setCleanupState({ status: 'schedule-success' }) - toast.success('Scheduled daily cleanup job.') - } catch (error) { - console.error('[CronJobs] Failed to schedule cleanup with error: %O', error) - const errorMessage = error instanceof Error ? error.message : 'Unknown error' - setCleanupState({ status: 'schedule-error', error: errorMessage }) - toast.error('Scheduling the cleanup job failed. Please try again.') - } + scheduleCronJobCleanup( + { projectRef, connectionString, interval }, + { + onSuccess: () => { + toast.success('Scheduled daily cleanup job.') + onSuccess?.() + }, + } + ) }, - [projectRef, connectionString, executeSql] + [connectionString, projectRef, scheduleCronJobCleanup] ) /** @@ -196,8 +189,10 @@ export const useCronJobsCleanupActions = ({ return { cleanupInterval, - setCleanupInterval, cleanupState, + isScheduling, + isScheduleSuccess, + setCleanupInterval, runBatchedDeletion, scheduleCleanup, cancelDeletion, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts index b982135674f..1939b8895be 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts @@ -1,16 +1,17 @@ import { keepPreviousData } from '@tanstack/react-query' -import { useMemo } from 'react' - -import type { ConnectionVars } from '@/data/common.types' -import { useCronJobRunDetailsEstimateQuery } from 'data/database-cron-jobs/database-cron-job-run-details-estimate-query' import { useCronJobsCountEstimateQuery } from 'data/database-cron-jobs/database-cron-jobs-count-estimate-query' import { useCronJobsCountQuery } from 'data/database-cron-jobs/database-cron-jobs-count-query' import { CronJob, useCronJobsInfiniteQuery, } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' +import { useMemo } from 'react' import type { ResponseError } from 'types' -import { CRON_JOBS_THRESHOLD, JOB_RUN_DETAILS_THRESHOLD } from './CronJobsTab.constants' + +import { CRON_JOBS_THRESHOLD } from './CronJobsTab.constants' +import type { ConnectionVars } from '@/data/common.types' +import { useCronJobsMinimalInfiniteQuery } from '@/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query' +import { COST_THRESHOLD_ERROR } from '@/data/sql/execute-sql-query' // ============================================================================= // Input types @@ -31,6 +32,7 @@ interface CronJobsGridState { isRefetching: boolean isFetchingNextPage: boolean hasNextPage: boolean + isMinimal: boolean refetch: () => void fetchNextPage: () => void } @@ -41,81 +43,16 @@ interface CronJobsCountState { isLoading: boolean } -// ============================================================================= -// Discriminated union for data fetching status -// ============================================================================= - -/** Still checking the size of the job_run_details table */ -interface StatusLoading { - status: 'loading' -} - -/** Failed to check the table size - show error with retry option */ -interface StatusEstimateError { - status: 'estimate-error' - error: Error - isRetrying: boolean - retry: () => void -} - -/** Table is confirmed to be too large - show cleanup notice */ -interface StatusOverflowConfirmed { - status: 'overflow-confirmed' - estimatedRows: number -} - -/** Query timed out - suspected large table, show cleanup notice */ -interface StatusOverflowSuspected { - status: 'overflow-suspected' - estimatedRows: number | undefined -} - -/** Normal state - queries can run */ -interface StatusReady { - status: 'ready' -} - -type CronJobsDataStatus = - | StatusLoading - | StatusEstimateError - | StatusOverflowConfirmed - | StatusOverflowSuspected - | StatusReady - // ============================================================================= // Result type // ============================================================================= interface UseCronJobsDataResult { - /** Discriminated union indicating the current data fetching status */ - dataStatus: CronJobsDataStatus - /** Whether queries are enabled (not gated due to large table or loading) */ - isQueryEnabled: boolean /** State for the cron jobs grid */ grid: CronJobsGridState /** State for the cron jobs count (exact or estimate) */ count: CronJobsCountState } - -// ============================================================================= -// Hook implementation -// ============================================================================= - -/** - * Custom hook that encapsulates all data fetching logic for the CronJobsTab. - * - * This hook manages the complex query dependencies: - * 1. First, estimate the size of the job_run_details table - * 2. If the table is small enough, fetch the actual cron jobs - * 3. If the table is too large, show estimates instead and display a cleanup notice - * - * The `dataStatus` discriminated union ensures type-safe handling of all states: - * - 'loading': Still checking table size - * - 'estimate-error': Failed to check, show retry option - * - 'overflow-confirmed': Table too large (from estimate) - * - 'overflow-suspected': Query timed out - * - 'ready': Normal operation - */ export function useCronJobsData({ projectRef, connectionString, @@ -123,37 +60,10 @@ export function useCronJobsData({ }: UseCronJobsDataParams): UseCronJobsDataResult { const isProjectReady = !!projectRef - const { - data: runDetailsEstimateValue, - error: runDetailsEstimateError, - isError: isRunDetailsEstimateError, - isPending: isRunDetailsEstimatePending, - isRefetching: isRefetchingRunDetailsEstimate, - refetch: refetchRunDetailsEstimate, - } = useCronJobRunDetailsEstimateQuery( - { projectRef, connectionString }, - { enabled: isProjectReady } - ) - - const hasLargeRunDetailsTable = - typeof runDetailsEstimateValue === 'number' && - runDetailsEstimateValue > JOB_RUN_DETAILS_THRESHOLD - - // Queries are enabled only when: - // - Project is ready - // - Estimate query has completed successfully - // - Table is not too large - const isQueryEnabled = - isProjectReady && - !isRunDetailsEstimatePending && - !isRunDetailsEstimateError && - !hasLargeRunDetailsTable - const { data: cronJobsData, error: cronJobsError, isLoading: isCronJobsLoading, - isError: isCronJobsError, isRefetching: isCronJobsRefetching, isFetchingNextPage, hasNextPage = false, @@ -164,14 +74,35 @@ export function useCronJobsData({ { placeholderData: Boolean(searchQuery) ? keepPreviousData : undefined, staleTime: Infinity, - enabled: isQueryEnabled, + } + ) + const useMinimalQuery = cronJobsError?.message === COST_THRESHOLD_ERROR + + const { + data: cronJobsMinimalData, + error: cronJobsMinimalError, + isLoading: isCronJobsMinimalLoading, + isRefetching: isCronJobsMinimalRefetching, + isFetchingNextPage: isFetchingNextPageMinimal, + hasNextPage: hasNextPageMinimal = false, + refetch: refetchCronJobsMinimal, + fetchNextPage: fetchNextPageMinimal, + } = useCronJobsMinimalInfiniteQuery( + { projectRef, connectionString, searchTerm: searchQuery ?? undefined }, + { + placeholderData: Boolean(searchQuery) ? keepPreviousData : undefined, + staleTime: Infinity, + enabled: useMinimalQuery, } ) - const cronJobs = useMemo( - () => cronJobsData?.pages.flatMap((page) => page) ?? [], - [cronJobsData?.pages] - ) + const cronJobs = useMemo(() => { + if (useMinimalQuery) { + return cronJobsMinimalData?.pages.flatMap((page) => page) ?? [] + } else { + return cronJobsData?.pages.flatMap((page) => page) ?? [] + } + }, [useMinimalQuery, cronJobsData?.pages, cronJobsMinimalData?.pages]) // Fetch count - gated on cron.job table size // Always fetch the estimate first (it's fast since it uses pg_stat) @@ -193,77 +124,18 @@ export function useCronJobsData({ { enabled: isCountQueryEnabled } ) - // Determine if we should show a timeout error as a suspected overflow - const isTimeoutError = - isCronJobsError && - typeof cronJobsError?.message === 'string' && - cronJobsError.message.toLowerCase().includes('timeout') - - // Compute the discriminated status - const dataStatus: CronJobsDataStatus = useMemo(() => { - if (!isProjectReady || isRunDetailsEstimatePending) { - return { status: 'loading' } - } - - if (isRunDetailsEstimateError) { - return { - status: 'estimate-error', - error: runDetailsEstimateError, - isRetrying: isRefetchingRunDetailsEstimate, - retry: refetchRunDetailsEstimate, - } - } - - if (hasLargeRunDetailsTable) { - return { - status: 'overflow-confirmed', - estimatedRows: runDetailsEstimateValue!, - } - } - - if (isTimeoutError) { - return { - status: 'overflow-suspected', - estimatedRows: runDetailsEstimateValue, - } - } - - return { status: 'ready' } - }, [ - isProjectReady, - isRunDetailsEstimatePending, - isRunDetailsEstimateError, - runDetailsEstimateError, - isRefetchingRunDetailsEstimate, - refetchRunDetailsEstimate, - hasLargeRunDetailsTable, - runDetailsEstimateValue, - isTimeoutError, - ]) - - // Compute derived grid state - const isGridLoading = - dataStatus.status === 'loading' || (dataStatus.status === 'ready' && isCronJobsLoading) - const gridError = - dataStatus.status === 'ready' && isCronJobsError && !isTimeoutError - ? cronJobsError ?? null - : null - return { - dataStatus, - isQueryEnabled, - grid: { rows: cronJobs, - isLoading: isGridLoading, - error: gridError, - isRefetching: isCronJobsRefetching, - isFetchingNextPage, - hasNextPage, - refetch: refetchCronJobs, - fetchNextPage, + error: useMinimalQuery ? cronJobsMinimalError : cronJobsError, + isLoading: useMinimalQuery ? isCronJobsMinimalLoading : isCronJobsLoading, + isRefetching: useMinimalQuery ? isCronJobsMinimalRefetching : isCronJobsRefetching, + isFetchingNextPage: useMinimalQuery ? isFetchingNextPageMinimal : isFetchingNextPage, + hasNextPage: useMinimalQuery ? hasNextPageMinimal : hasNextPage, + isMinimal: useMinimalQuery, + refetch: useMinimalQuery ? refetchCronJobsMinimal : refetchCronJobs, + fetchNextPage: useMinimalQuery ? fetchNextPageMinimal : fetchNextPage, }, - count: { value: isCountQueryEnabled ? exactCount : estimatedCount, isEstimate: !isCountQueryEnabled, @@ -271,9 +143,3 @@ export function useCronJobsData({ }, } } - -// ============================================================================= -// Type exports for consumers -// ============================================================================= - -export type { CronJobsCountState, CronJobsDataStatus, CronJobsGridState } diff --git a/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts b/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts deleted file mode 100644 index 89b40815049..00000000000 --- a/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { QueryClient, useQuery } from '@tanstack/react-query' - -import type { ConnectionVars } from 'data/common.types' -import { executeSql } from 'data/sql/execute-sql-query' -import { - getLiveTupleEstimate, - getLiveTupleEstimateKey, -} from 'data/sql/queries/get-live-tuple-stats' -import type { UseCustomQueryOptions } from 'types' - -type DatabaseCronJobRunDetailsEstimateVariables = ConnectionVars - -const cronJobRunDetailsEstimateSql = getLiveTupleEstimate('job_run_details', 'cron') -const cronJobRunDetailsEstimateKey = (projectRef: string | undefined) => - getLiveTupleEstimateKey(projectRef, 'job_run_details', 'cron') - -export async function getCronJobRunDetailsEstimate({ - projectRef, - connectionString, -}: DatabaseCronJobRunDetailsEstimateVariables) { - if (!projectRef) throw new Error('Project ref is required') - - const { result } = await executeSql<{ live_tuple_estimate: number }[]>({ - projectRef, - connectionString, - sql: cronJobRunDetailsEstimateSql, - queryKey: cronJobRunDetailsEstimateKey(projectRef), - }) - - return result?.[0]?.live_tuple_estimate -} - -export type DatabaseCronJobRunDetailsEstimateData = Awaited< - ReturnType -> -export type DatabaseCronJobRunDetailsEstimateError = Error - -export const useCronJobRunDetailsEstimateQuery = ( - { projectRef, connectionString }: DatabaseCronJobRunDetailsEstimateVariables, - { - enabled = true, - ...options - }: UseCustomQueryOptions< - DatabaseCronJobRunDetailsEstimateData, - DatabaseCronJobRunDetailsEstimateError, - TData - > = {} -) => - useQuery({ - queryKey: cronJobRunDetailsEstimateKey(projectRef), - queryFn: () => getCronJobRunDetailsEstimate({ projectRef, connectionString }), - enabled: enabled && projectRef !== undefined, - staleTime: 5 * 60 * 1000, - ...options, - }) - -export function prefetchCronJobRunDetailsEstimate( - client: QueryClient, - { projectRef, connectionString }: DatabaseCronJobRunDetailsEstimateVariables -) { - return client.fetchQuery({ - // Does not change if connection string changes - // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: cronJobRunDetailsEstimateKey(projectRef), - queryFn: () => getCronJobRunDetailsEstimate({ projectRef, connectionString }), - }) -} diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts index fb8d737f4bf..84cd0ee9c33 100644 --- a/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts +++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts @@ -1,12 +1,13 @@ import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query' - -import { executeSql } from 'data/sql/execute-sql-query' +import { COST_THRESHOLD_ERROR, executeSql } from 'data/sql/execute-sql-query' import type { ResponseError, UseCustomInfiniteQueryOptions } from 'types' + +import { getCronJobsSql } from '../sql/queries/get-cron-jobs' import { databaseCronJobsKeys } from './keys' -const CRON_JOBS_PAGE_LIMIT = 20 +export const CRON_JOBS_PAGE_LIMIT = 20 -type DatabaseCronJobRunsVariables = { +export type DatabaseCronJobRunsVariables = { projectRef?: string connectionString?: string | null searchTerm?: string @@ -17,50 +18,11 @@ export type CronJob = { jobname: string | null active: boolean command: string - latest_run: string schedule: string - status: string + latest_run?: string + status?: string } -// [Joshen] Just to call out that I had AI help me with this, so please let me know if this can be optimized -const getCronJobSql = ({ searchTerm, page }: { searchTerm?: string; page: number }) => - ` -WITH latest_runs AS ( - SELECT - jobid, - status, - MAX(start_time) AS latest_run - FROM cron.job_run_details - GROUP BY jobid, status -), most_recent_runs AS ( - SELECT - jobid, - status, - latest_run - FROM latest_runs lr1 - WHERE latest_run = ( - SELECT MAX(latest_run) - FROM latest_runs lr2 - WHERE lr2.jobid = lr1.jobid - ) -) -SELECT - job.jobid, - job.jobname, - job.schedule, - job.command, - job.active, - mr.latest_run, - mr.status -FROM - cron.job job -LEFT JOIN most_recent_runs mr ON job.jobid = mr.jobid -${!!searchTerm ? `WHERE job.jobname ILIKE '%${searchTerm}%'` : ''} -ORDER BY job.jobid -LIMIT ${CRON_JOBS_PAGE_LIMIT} -OFFSET ${page * CRON_JOBS_PAGE_LIMIT}; -`.trim() - export async function getDatabaseCronJobs({ projectRef, connectionString, @@ -72,8 +34,9 @@ export async function getDatabaseCronJobs({ const { result } = await executeSql({ projectRef, connectionString, - sql: getCronJobSql({ searchTerm, page }), + sql: getCronJobsSql({ searchTerm, page, limit: CRON_JOBS_PAGE_LIMIT }), queryKey: ['cron-jobs'], + preflightCheck: true, }) return result @@ -114,5 +77,9 @@ export const useCronJobsInfiniteQuery = ( if (!hasNextPage) return undefined return page }, + retry: (failureCount, error) => { + if (error.message === COST_THRESHOLD_ERROR) return false + return failureCount < 3 + }, ...options, }) diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts new file mode 100644 index 00000000000..26106ae581a --- /dev/null +++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts @@ -0,0 +1,67 @@ +import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query' +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError, UseCustomInfiniteQueryOptions } from 'types' + +import { getCronJobsMinimalSql } from '../sql/queries/get-cron-jobs' +import { + CRON_JOBS_PAGE_LIMIT, + CronJob, + DatabaseCronJobRunsVariables, +} from './database-cron-jobs-infinite-query' +import { databaseCronJobsKeys } from './keys' + +export async function getDatabaseCronJobsMinimal({ + projectRef, + connectionString, + searchTerm, + page = 0, +}: DatabaseCronJobRunsVariables & { page: number }) { + if (!projectRef) throw new Error('Project ref is required') + + const { result } = await executeSql({ + projectRef, + connectionString, + sql: getCronJobsMinimalSql({ searchTerm, page, limit: CRON_JOBS_PAGE_LIMIT }), + queryKey: ['cron-jobs-minimal'], + }) + + return result +} + +type DatabaseCronJobsMinimalInfiniteData = CronJob[] +type DatabaseCronJobsMinimalInfiniteError = ResponseError + +export const useCronJobsMinimalInfiniteQuery = ( + { projectRef, connectionString, searchTerm }: DatabaseCronJobRunsVariables, + { + enabled = true, + ...options + }: UseCustomInfiniteQueryOptions< + DatabaseCronJobsMinimalInfiniteData, + DatabaseCronJobsMinimalInfiniteError, + InfiniteData, + readonly unknown[], + number + > = {} +) => + useInfiniteQuery({ + queryKey: databaseCronJobsKeys.listInfiniteMinimal(projectRef, searchTerm), + queryFn: ({ pageParam }) => { + return getDatabaseCronJobsMinimal({ + projectRef, + connectionString, + searchTerm, + page: pageParam, + }) + }, + staleTime: 0, + enabled: enabled && typeof projectRef !== 'undefined', + initialPageParam: 0, + getNextPageParam(lastPage, pages) { + const page = pages.length + const hasNextPage = lastPage.length >= CRON_JOBS_PAGE_LIMIT + if (!hasNextPage) return undefined + return page + }, + ...options, + }) diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts index 6a6505fb085..964542f2c35 100644 --- a/apps/studio/data/database-cron-jobs/keys.ts +++ b/apps/studio/data/database-cron-jobs/keys.ts @@ -6,6 +6,8 @@ export const databaseCronJobsKeys = { ['projects', projectRef, 'cron-jobs', identifier] as const, listInfinite: (projectRef: string | undefined, searchTerm: string | undefined) => ['projects', projectRef, 'cron-jobs', { searchTerm }] as const, + listInfiniteMinimal: (projectRef: string | undefined, searchTerm: string | undefined) => + ['projects', projectRef, 'cron-jobs-mininal', { searchTerm }] as const, count: (projectRef: string | undefined) => ['projects', projectRef, 'cron-jobs', 'count'] as const, run: (projectRef: string | undefined, jobId: number) => [ diff --git a/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts b/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts new file mode 100644 index 00000000000..7923c247524 --- /dev/null +++ b/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts @@ -0,0 +1,66 @@ +import { useMutation } from '@tanstack/react-query' +import { executeSql } from 'data/sql/execute-sql-query' +import { toast } from 'sonner' +import type { ResponseError, UseCustomMutationOptions } from 'types' + +import { + getScheduleDeleteCronJobRunDetailsKey, + getScheduleDeleteCronJobRunDetailsSql, +} from '../sql/queries/delete-cron-job-run-details' + +export type ScheduleCronJobRunDetailsCleanupVariables = { + projectRef: string + connectionString?: string | null + interval: string +} + +export async function scheduleCronJobRunDetailsCleanup({ + projectRef, + connectionString, + interval, +}: ScheduleCronJobRunDetailsCleanupVariables) { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: getScheduleDeleteCronJobRunDetailsSql(interval), + queryKey: getScheduleDeleteCronJobRunDetailsKey(projectRef, interval), + }) + + return result +} + +type ScheduleCronJobRunDetailsCleanupData = Awaited< + ReturnType +> + +export const useScheduleCronJobRunDetailsCleanupMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseCustomMutationOptions< + ScheduleCronJobRunDetailsCleanupData, + ResponseError, + ScheduleCronJobRunDetailsCleanupVariables + >, + 'mutationFn' +> = {}) => { + return useMutation< + ScheduleCronJobRunDetailsCleanupData, + ResponseError, + ScheduleCronJobRunDetailsCleanupVariables + >({ + mutationFn: (vars) => scheduleCronJobRunDetailsCleanup(vars), + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to schedule clean up job: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/sql/queries/get-cron-jobs.ts b/apps/studio/data/sql/queries/get-cron-jobs.ts new file mode 100644 index 00000000000..aa930819ca7 --- /dev/null +++ b/apps/studio/data/sql/queries/get-cron-jobs.ts @@ -0,0 +1,72 @@ +import { literal } from '@supabase/pg-meta/src/pg-format' + +// [Joshen] Just omits the LEFT JOIN as that's the heavy part +export const getCronJobsMinimalSql = ({ + searchTerm, + page, + limit, +}: { + searchTerm?: string + page: number + limit: number +}) => + ` +SELECT + job.jobid, + job.jobname, + job.schedule, + job.command, + job.active +FROM + cron.job job +${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''} +ORDER BY job.jobid +LIMIT ${limit} +OFFSET ${page * limit}; +`.trim() + +export const getCronJobsSql = ({ + searchTerm, + page, + limit, +}: { + searchTerm?: string + page: number + limit: number +}) => + ` +WITH latest_runs AS ( + SELECT + jobid, + status, + MAX(start_time) AS latest_run + FROM cron.job_run_details + GROUP BY jobid, status +), most_recent_runs AS ( + SELECT + jobid, + status, + latest_run + FROM latest_runs lr1 + WHERE latest_run = ( + SELECT MAX(latest_run) + FROM latest_runs lr2 + WHERE lr2.jobid = lr1.jobid + ) +) +SELECT + job.jobid, + job.jobname, + job.schedule, + job.command, + job.active, + mr.latest_run, + mr.status +FROM + cron.job job +LEFT JOIN most_recent_runs mr ON job.jobid = mr.jobid +${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''} +ORDER BY job.jobid +LIMIT ${limit} +OFFSET ${page * limit}; +`.trim() diff --git a/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts b/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts deleted file mode 100644 index dc894ff1f92..00000000000 --- a/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useEffect } from 'react' - -import { prefetchCronJobRunDetailsEstimate } from 'data/database-cron-jobs/database-cron-job-run-details-estimate-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useStaticEffectEvent } from 'hooks/useStaticEffectEvent' - -/** - * Prefetches the cron job run details estimate when on a Cron integration page. - * This avoids a query waterfall when navigating to the Jobs tab, since the - * Jobs tab gates loading cron jobs on this estimate query completing first. - */ -export function useCronJobsEstimatePrefetch(integrationId: string | undefined) { - const queryClient = useQueryClient() - const { data: project } = useSelectedProjectQuery() - - const prefetch = useStaticEffectEvent(() => { - prefetchCronJobRunDetailsEstimate(queryClient, { - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - }) - - useEffect(() => { - if (integrationId === 'cron' && project?.ref) { - prefetch() - } - }, [integrationId, project?.ref, prefetch]) -} diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx index 0e83542b46e..4388c94e708 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx @@ -1,12 +1,10 @@ -import { useRouter } from 'next/router' -import { useEffect, useMemo } from 'react' - import { useParams } from 'common' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' -import { useCronJobsEstimatePrefetch } from 'hooks/misc/useCronJobsEstimatePrefetch' +import { useRouter } from 'next/router' +import { useEffect, useMemo } from 'react' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns' import { PageContainer } from 'ui-patterns/PageContainer' @@ -24,8 +22,6 @@ const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() const { ref, id, pageId, childId } = useParams() - useCronJobsEstimatePrefetch(id) - const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } = useInstalledIntegrations() diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 421bb715e9b..ee4fdac3905 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -1,15 +1,13 @@ -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useMemo } from 'react' - import { useFlag, useParams } from 'common' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' import { UnknownInterface } from 'components/ui/UnknownInterface' -import { useCronJobsEstimatePrefetch } from 'hooks/misc/useCronJobsEstimatePrefetch' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useMemo } from 'react' import type { NextPageWithLayout } from 'types' import { BreadcrumbItem_Shadcn_ as BreadcrumbItem, @@ -44,8 +42,6 @@ const IntegrationPage: NextPageWithLayout = () => { const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration') - useCronJobsEstimatePrefetch(id) - const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } = useInstalledIntegrations() diff --git a/e2e/studio/features/cron-jobs.spec.ts b/e2e/studio/features/cron-jobs.spec.ts index 6876549ba12..4dac263157f 100644 --- a/e2e/studio/features/cron-jobs.spec.ts +++ b/e2e/studio/features/cron-jobs.spec.ts @@ -87,10 +87,10 @@ const deleteCronJob = async (page: Page, jobName: string) => { await expect(jobRow).not.toBeVisible({ timeout: 10000 }) } -// Run all cron tests serially since they share database state (pg_cron extension) -test.describe.configure({ mode: 'serial' }) - test.describe('Cron Jobs Integration', () => { + // Run CRUD tests serially since they share database state (pg_cron extension) + test.describe.configure({ mode: 'serial' }) + test.describe('Cron Jobs CRUD Operations', () => { let page: Page @@ -266,56 +266,194 @@ test.describe('Cron Jobs Integration', () => { }) }) -test.describe('Large Table Overflow Notice', () => { - const cleanupJobName = 'delete-job-run-details' +test.describe('High Query Cost Banner', () => { + const testJobName = 'pw_high_cost_test_job' - test('shows cleanup notice and allows cleanup workflow', async ({ page, ref }) => { - // First ensure pg_cron is enabled (without any mocks) + test.describe.configure({ mode: 'serial' }) + + test('shows banner and still displays cron jobs when query cost exceeds threshold', async ({ + page, + ref, + }) => { + // First ensure pg_cron is enabled await navigateToCronOverviewAndEnable(page, ref) + await navigateToCronJobsPage(page, ref) - // Now set up the mock for the row count estimate + // Check if test job already exists (from previous test run) + const existingTestJob = page.getByRole('row', { name: new RegExp(testJobName) }) + const jobExists = (await existingTestJob.count()) > 0 + + // Create a test cron job only if it doesn't exist + if (!jobExists) { + await page.getByRole('button', { name: 'Create job' }).click() + await expect(page.getByRole('heading', { name: 'Create a new cron job' })).toBeVisible() + await page.locator('input[name="name"]').fill(testJobName) + await page.getByRole('button', { name: 'Every minute' }).click() + await page.getByRole('code').click() + await page.getByRole('textbox', { name: /Editor content/ }).fill("SELECT 'high_cost_test';") + await page.getByRole('button', { name: 'Create cron job' }).click() + await expect(page.getByText(/Successfully created cron job/)).toBeVisible({ timeout: 10000 }) + } + + // Now set up the mock for the EXPLAIN query (preflight check) to return high cost + // This simulates the scenario where cron.job_run_details is too large await page.route('**/pg-meta/*/query**', async (route) => { const request = route.request() const postData = request.postDataJSON() - // Only intercept the live tuple estimate query for job_run_details - if (postData?.query?.includes('n_live_tup') && postData?.query?.includes('job_run_details')) { + // Intercept EXPLAIN queries for the cron jobs query (the preflight check) + if ( + postData?.query?.toLowerCase().startsWith('explain') && + postData?.query?.includes('cron.job') + ) { + // Return a mock EXPLAIN result with very high cost (over 100,000 threshold) await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify([{ live_tuple_estimate: 500000 }]), + body: JSON.stringify([ + { 'QUERY PLAN': 'Nested Loop Left Join (cost=0.00..500000.00 rows=1000000 width=100)' }, + ]), }) } else { await route.continue() } }) - // Navigate to the cron jobs page (this will trigger the mocked estimate query) + // Navigate to the cron jobs page - this will trigger the mocked preflight check await page.goto(toUrl(`/project/${ref}/integrations/cron/jobs`)) - // Should show the overflow notice with the warning title - await expect(page.getByText('cron.job_run_details is too large to load')).toBeVisible({ + // Wait for the grid to load - this verifies cron jobs are still visible in minimal mode + await expect(page.getByRole('grid')).toBeVisible({ timeout: 30000 }) + + // Should show the overflow banner (minimal mode indicator) + await expect( + page.getByText('Last run for each cron job omitted due to high query cost') + ).toBeVisible({ timeout: 15000, }) - // Should show the estimated row count in the description - await expect(page.getByText(/approximately 500,000 rows/)).toBeVisible() + // The test job should still be visible in the grid (minimal mode shows jobs without last run) + await expect(page.getByRole('row', { name: new RegExp(testJobName) })).toBeVisible() + + // The "Learn more" button should be visible + await expect(page.getByRole('button', { name: 'Learn more' })).toBeVisible() + + // Remove the route mock for subsequent tests + await page.unroute('**/pg-meta/*/query**') + }) + + test('Learn more dialog shows cleanup options', async ({ page, ref }) => { + // Set up the mock again for this test + await page.route('**/pg-meta/*/query**', async (route) => { + const request = route.request() + const postData = request.postDataJSON() + + if ( + postData?.query?.toLowerCase().startsWith('explain') && + postData?.query?.includes('cron.job') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { 'QUERY PLAN': 'Nested Loop Left Join (cost=0.00..500000.00 rows=1000000 width=100)' }, + ]), + }) + } else { + await route.continue() + } + }) + + await page.goto(toUrl(`/project/${ref}/integrations/cron/jobs`)) + + // Wait for the banner to appear + await expect( + page.getByText('Last run for each cron job omitted due to high query cost') + ).toBeVisible({ + timeout: 15000, + }) + + // Click the "Learn more" button to open the dialog + await page.getByRole('button', { name: 'Learn more' }).click() + + // The dialog should open with the explanation + await expect( + page.getByRole('heading', { name: 'Last run for cron jobs omitted for overview' }) + ).toBeVisible() + + // Should explain the issue + await expect( + page.getByText(/the estimated query cost exceeds safety thresholds/) + ).toBeVisible() // Should show Step 1 with cleanup options await expect(page.getByText('Step 1: Delete older entries')).toBeVisible() await expect(page.getByRole('button', { name: 'Delete rows now' })).toBeVisible() + // Should show the interval selector + await expect(page.getByRole('combobox')).toBeVisible() + // Should show Step 2 (disabled until step 1 is complete) await expect(page.getByText('Step 2: Schedule an automated cleanup')).toBeVisible() - await expect(page.getByText('Complete step 1 to enable scheduling')).toBeVisible() + await expect( + page.getByText('Complete step 1 to enable scheduling a daily cleanup job') + ).toBeVisible() + + // Remove the route mock + await page.unroute('**/pg-meta/*/query**') + }) + + test('cleanup workflow: delete rows and schedule cleanup job', async ({ page, ref }) => { + // Set up the mock for the high cost scenario + await page.route('**/pg-meta/*/query**', async (route) => { + const request = route.request() + const postData = request.postDataJSON() + + if ( + postData?.query?.toLowerCase().startsWith('explain') && + postData?.query?.includes('cron.job') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { 'QUERY PLAN': 'Nested Loop Left Join (cost=0.00..500000.00 rows=1000000 width=100)' }, + ]), + }) + } else { + await route.continue() + } + }) + + await page.goto(toUrl(`/project/${ref}/integrations/cron/jobs`)) + + // Wait for the banner and click Learn more + await expect( + page.getByText('Last run for each cron job omitted due to high query cost') + ).toBeVisible({ + timeout: 15000, + }) + await page.getByRole('button', { name: 'Learn more' }).click() + + // Wait for dialog to open + await expect( + page.getByRole('heading', { name: 'Last run for cron jobs omitted for overview' }) + ).toBeVisible() // Step 1: Click "Delete rows now" to run the cleanup await page.getByRole('button', { name: 'Delete rows now' }).click() // Wait for Step 1 to complete - should show success message + // Note: In a fresh DB, there might be 0 rows deleted, which is still success await expect(page.getByText(/Successfully deleted \d+ rows/)).toBeVisible({ timeout: 30000 }) - // Step 2 should now be enabled - click "Schedule cleanup job" + // Step 2 should now be enabled - the "Schedule cleanup job" button should be visible + await expect(page.getByRole('button', { name: 'Schedule cleanup job' })).toBeVisible() + + // Should show the SQL preview for the cleanup job + await expect(page.getByText(/cron\.schedule/)).toBeVisible() + + // Click "Schedule cleanup job" await page.getByRole('button', { name: 'Schedule cleanup job' }).click() // Wait for Step 2 to complete - should show success message @@ -323,14 +461,11 @@ test.describe('Large Table Overflow Notice', () => { timeout: 15000, }) - // Clean up: remove the route mock and delete the scheduled cleanup job + // Clean up: remove the route mock await page.unroute('**/pg-meta/*/query**') - // Click refresh to reload the page with real data - await page.getByRole('button', { name: 'Refresh' }).last().click() - - // Wait for the grid to appear and verify the cleanup job was created, then delete it - await expect(page.getByRole('grid')).toBeVisible({ timeout: 30000 }) - await expect(page.getByRole('row', { name: new RegExp(cleanupJobName) })).toBeVisible() + // Note: Test jobs (pw_high_cost_test_job, delete-job-run-details) will be cleaned up + // in the next test run by the CRUD tests' beforeAll cleanup, or manually. + // Attempting to delete here causes pointer events issues with Radix dialogs. }) }) From e803507a0dcfcb80e2a3b9e7f9f6422a23552a39 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Feb 2026 11:47:43 +0800 Subject: [PATCH 11/22] Add flag disabled UI handler for code redeem page (#42591) ## Context Adds a UI state for when the feature flag for code redeem is disabled image ## Other changes involved Mostly refactor + clean up here - Pass `onClick` event to `OrganizationCard`, rather than declaring an extra parent `div` to handle the click event - Use `Admonition` instead of the `Alert` components within `CreditCodeRedemption` - Adjust element semantics in `CreditCodeRedemption` (Opting for `p` instead of `h4` and `h1` to show credit related information) - Use `UpgradePlanButton` component for upgrade CTA (Handles behaviour depending if user has perms to update plan of the organization) - Use `TimestampInfo` component for expires at - Fix error handling when calling `redeemCode` (use `mutate` instead of `mutateAsync` which requires a try catch) ## To Test - [ ] Need to verify that the `TimestampInfo` component renders correctly after redeeming a code ## Summary by CodeRabbit * **New Features** * Organization selection with modal-driven credit code redemption and URL prefill. * Upgrade button accepts an organization slug and integrates into the upgrade flow. * Organization cards support an optional click handler for custom interactions. * **UI/UX Improvements** * Success/error messaging switched to admonition styling; expiry dates shown with timestamps. * Horizontal form layout, medium dialog sizing, compact balance/code presentation, and invisible CAPTCHA with reset on success. * Feature gated by a flag with clearer loading/error states. --- .../BillingSettings/CreditCodeRedemption.tsx | 208 ++++++++---------- .../Organization/OrganizationCard.tsx | 3 + .../ui/RequestUpgradeToBillingOwners.tsx | 9 +- .../components/ui/UpgradePlanButton.tsx | 14 +- ...ization-credit-code-redemption-mutation.ts | 9 +- apps/studio/pages/redeem.tsx | 101 +++++---- 6 files changed, 177 insertions(+), 167 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx index d34d78f8720..968e63bf7cc 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx @@ -1,19 +1,15 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Alert, AlertDescription, AlertTitle } from '@ui/components/shadcn/ui/alert' import { useFlag } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { AlertCircle, Calendar, PartyPopper } from 'lucide-react' +import { Calendar, PartyPopper } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Dialog, DialogContent, @@ -24,19 +20,20 @@ import { DialogSectionSeparator, DialogTitle, DialogTrigger, - FormField_Shadcn_, Form_Shadcn_, + FormField_Shadcn_, Input_Shadcn_, Separator, } from 'ui' -import { ShimmeringLoader } from 'ui-patterns' +import { Admonition, ShimmeringLoader, TimestampInfo } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' +import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton' import { useOrganizationCreditCodeRedemptionMutation } from '@/data/organizations/organization-credit-code-redemption-mutation' import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query' import { useOrganizationQuery } from '@/data/organizations/organization-query' -import useLatest from '@/hooks/misc/useLatest' +import { useLatest } from '@/hooks/misc/useLatest' const FORM_ID = 'credit-code-redemption' @@ -51,59 +48,49 @@ export const CreditCodeRedemption = ({ modalVisible = false, onClose, }: { - slug: string | undefined + slug?: string modalVisible?: boolean onClose?: () => void }) => { + const router = useRouter() + const redeemCodeEnabled = useFlag('redeemCodeEnabled') + const [codeRedemptionModalVisible, setCodeRedemptionModalVisible] = useState( + modalVisible || false + ) + + const { data: org, isLoading: isOrgLoading } = useOrganizationQuery({ slug }) + const { data: customerProfile, isLoading: isCustomerProfileLoading } = + useOrganizationCustomerProfileQuery({ slug }) + const { can: canRedeemCode, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions', undefined, - { - organizationSlug: slug, - } + { organizationSlug: slug } ) - const redeemCodeEnabled = useFlag('redeemCodeEnabled') - - const { data: org, isLoading: isOrgLoading } = useOrganizationQuery({ slug }) - - const router = useRouter() + const captchaRef = useRef(null) + const captchaTokenRef = useRef(null) + const codeRedemptionDisabled = + !canRedeemCode || !isPermissionsLoaded || isOrgLoading || isCustomerProfileLoading const form = useForm({ resolver: zodResolver(FormSchema), - defaultValues: { - code: '', - }, + defaultValues: { code: '' }, }) - const { data: customerProfile, isLoading: isCustomerProfileLoading } = - useOrganizationCustomerProfileQuery({ slug }) - - useEffect(() => { - if (!router.isReady) return - - const queryCode = router.query.code - const codeFromParams = Array.isArray(queryCode) ? queryCode[0] : queryCode - - if (typeof codeFromParams === 'string' && codeFromParams.trim().length > 2) { - form.setValue('code', codeFromParams) - } - }, [router.isReady, router.query.code, form]) - - const [codeRedemptionModalVisible, setCodeRedemptionModalVisible] = useState( - modalVisible || false - ) - const captchaRef = useRef(null) - const captchaTokenRef = useRef(null) - const { - mutateAsync: redeemCode, + mutate: redeemCode, isPending: redeemingCode, error: errorRedeemingCode, data: codeRedemptionResult, reset: resetCodeRedemption, - } = useOrganizationCreditCodeRedemptionMutation() + } = useOrganizationCreditCodeRedemptionMutation({ + onSuccess: () => { + form.setValue('code', '') + resetCaptcha() + }, + }) const resetCaptcha = () => { captchaTokenRef.current = null @@ -128,28 +115,9 @@ export const CreditCodeRedemption = ({ } const initHcaptchaRef = useLatest(initHcaptcha) - useEffect(() => { - if (codeRedemptionModalVisible) { - initHcaptchaRef.current() - } - }, [codeRedemptionModalVisible, initHcaptchaRef]) - const onSubmit: SubmitHandler = async ({ code }) => { const token = await initHcaptcha() - - await redeemCode( - { - slug, - code, - hcaptchaToken: token, - }, - { - onSuccess: () => { - form.setValue('code', '') - resetCaptcha() - }, - } - ) + redeemCode({ slug, code, hcaptchaToken: token }) } const onCodeRedemptionDialogVisibilityChange = (visible: boolean) => { @@ -161,12 +129,24 @@ export const CreditCodeRedemption = ({ } } - if (!redeemCodeEnabled) { - return null - } + useEffect(() => { + if (!router.isReady) return - const codeRedemptionDisabled = - !canRedeemCode || !isPermissionsLoaded || isOrgLoading || isCustomerProfileLoading + const queryCode = router.query.code + const codeFromParams = Array.isArray(queryCode) ? queryCode[0] : queryCode + + if (typeof codeFromParams === 'string' && codeFromParams.trim().length > 2) { + form.setValue('code', codeFromParams) + } + }, [router.isReady, router.query.code, form]) + + useEffect(() => { + if (codeRedemptionModalVisible) { + initHcaptchaRef.current() + } + }, [codeRedemptionModalVisible, initHcaptchaRef]) + + if (!redeemCodeEnabled) return null return ( @@ -191,7 +171,7 @@ export const CreditCodeRedemption = ({ )} - e.preventDefault()} size={'large'}> + e.preventDefault()}> - {codeRedemptionResult ? ( + {!!codeRedemptionResult ? (
- +
-

Credits Redeemed!

+

Credits redeemed!

-

$

-

{codeRedemptionResult.amount_cents / 100}

-

credits applied

+

$

+

{codeRedemptionResult.amount_cents / 100}

+

credits applied

@@ -234,26 +214,29 @@ export const CreditCodeRedemption = ({
- {'Expires on '} - {new Date(codeRedemptionResult.credits_expire_at).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} + Expires on{' '} +
)} - {org?.plan.id === 'free' && ( -
- -
- )} +
) : ( <> @@ -282,10 +265,10 @@ export const CreditCodeRedemption = ({ control={form.control} name="code" render={({ field }) => ( - + @@ -293,35 +276,34 @@ export const CreditCodeRedemption = ({ /> {customerProfile && customerProfile.balance < 0 && ( -
- Current Balance -
-

$

-

{customerProfile.balance / -100}

-

/credits

+
+ Current Balance +
+

$

+

{customerProfile.balance / -100}

+

/credits

)} - - - Potential future charges - - Credits are applied to {org?.name} only and can't be shared - or transferred to other organizations. Credits are automatically used toward - invoices. When credits run out on a paid plan, your default payment method - will be charged—your plan won't be downgraded automatically. - - + +

+ Credits are applied to {org?.name} only and cannot be + shared or transferred to other organizations. Credits are automatically used + toward invoices. +

+

+ When credits run out on a paid plan, your default payment method will be + charged—your plan won't be downgraded automatically. +

+
{errorRedeemingCode && ( - - - Code cannot be redeemed - - {errorRedeemingCode?.message} - - + )} diff --git a/apps/studio/components/interfaces/Organization/OrganizationCard.tsx b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx index a1e8abe1232..3e081c81ac3 100644 --- a/apps/studio/components/interfaces/Organization/OrganizationCard.tsx +++ b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx @@ -12,11 +12,13 @@ export const OrganizationCard = ({ href, isLink = true, className, + onClick, }: { organization: Organization href?: string isLink?: boolean className?: string + onClick?: () => void }) => { const isUserMFAEnabled = useIsMFAEnabled() const { data } = useOrgProjectsInfiniteQuery({ slug: organization.slug }) @@ -32,6 +34,7 @@ export const OrganizationCard = ({ )} icon={} title={organization.name} + onClick={onClick} description={
diff --git a/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx index 32e302cc630..5b4bf6735d5 100644 --- a/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx +++ b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx @@ -1,9 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { PropsWithChildren, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import z from 'zod' - import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' import { @@ -13,6 +8,9 @@ import { import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useTrack } from 'lib/telemetry/track' +import { PropsWithChildren, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { Badge, Button, @@ -34,6 +32,7 @@ import { TooltipTrigger, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import z from 'zod' const FormSchema = z.object({ note: z.string().optional(), diff --git a/apps/studio/components/ui/UpgradePlanButton.tsx b/apps/studio/components/ui/UpgradePlanButton.tsx index 7c9851d2b5e..186feac161e 100644 --- a/apps/studio/components/ui/UpgradePlanButton.tsx +++ b/apps/studio/components/ui/UpgradePlanButton.tsx @@ -1,13 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import Link from 'next/link' -import { PropsWithChildren } from 'react' - import { useFlag, useParams } from 'common' import { SupportLink } from 'components/interfaces/Support/SupportLink' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import Link from 'next/link' +import { PropsWithChildren } from 'react' import { Button } from 'ui' + import { ButtonTooltip } from './ButtonTooltip' import { RequestUpgradeToBillingOwners } from './RequestUpgradeToBillingOwners' @@ -24,6 +24,7 @@ interface UpgradePlanButtonProps { featureProposition?: string disabled?: boolean className?: string + slug?: string } /** @@ -40,18 +41,21 @@ export const UpgradePlanButton = ({ disabled, children, className, + slug: slugParam, }: PropsWithChildren) => { const { ref } = useParams() const { data: organization } = useSelectedOrganizationQuery() const isFreePlan = organization?.plan?.id === 'free' - const slug = organization?.slug ?? '_' + const slug = slugParam ?? organization?.slug ?? '_' const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') const { billingAll } = useIsFeatureEnabled(['billing:all']) const { can: canUpdateSubscription } = useAsyncCheckPermissions( PermissionAction.BILLING_WRITE, - 'stripe.subscriptions' + 'stripe.subscriptions', + undefined, + { organizationSlug: slug } ) const subject = `Enquiry to upgrade ${!!plan ? `to ${plan} ` : ''}plan for organization` diff --git a/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts b/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts index d8ece7ba77c..daf36dd9e8b 100644 --- a/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts +++ b/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts @@ -60,10 +60,13 @@ export const useOrganizationCreditCodeRedemptionMutation = ({ return useMutation({ mutationFn: (vars) => redeemCode(vars), async onSuccess(data, variables, context) { - const { slug, code } = variables + const { slug } = variables + + await Promise.all([ + queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) }), + queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) }), + ]) - await queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) }) - await queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) }) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/pages/redeem.tsx b/apps/studio/pages/redeem.tsx index 4518951f057..39156310bdf 100644 --- a/apps/studio/pages/redeem.tsx +++ b/apps/studio/pages/redeem.tsx @@ -1,7 +1,8 @@ +import { useFlag } from 'common' import Link from 'next/link' import { useState } from 'react' -import { Button, Loading } from 'ui' -import { ShimmeringLoader } from 'ui-patterns' +import { Button } from 'ui' +import { Admonition, ShimmeringLoader } from 'ui-patterns' import { CreditCodeRedemption } from '@/components/interfaces/Organization/BillingSettings/CreditCodeRedemption' import { OrganizationCard } from '@/components/interfaces/Organization/OrganizationCard' @@ -12,67 +13,85 @@ import { ScaffoldHeader, ScaffoldTitle, } from '@/components/layouts/Scaffold' +import AlertError from '@/components/ui/AlertError' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import type { NextPageWithLayout } from '@/types' const RedeemCreditsContent = () => { - const { - data: organizations, - isLoading: areOrganizationsLoading, - isFetched: isOrganizationsFetched, - isSuccess: wasOrganizationsRequestSuccessful, - } = useOrganizationsQuery() - + const redeemCodeEnabled = useFlag('redeemCodeEnabled') const [selectedOrg, setSelectedOrg] = useState(null) - if (!isOrganizationsFetched) { - return ( - -
- + const { + data: organizations, + error: errorOrganizations, + isLoading: isLoadingOrganizations, + isError: isErrorOrganizations, + } = useOrganizationsQuery() -
- - -
+ if (isLoadingOrganizations) { + return ( +
+ +
+ +
- +
) } - if (!wasOrganizationsRequestSuccessful) { - return

Error loading redeem credit page. Try again later.

+ if (isErrorOrganizations) { + return ( + + ) } return (
-
-

- To redeem your credits, select one of your organizations. The credits will be applied to - that organization only and cannot be transferred or shared between organizations. -

-

- Want to start fresh? Create a new - organization first. You will have to revisit this link after creating the organization to - redeem the code. -

- +
+
+

To redeem your credits, select one of your organizations.

+

+ The credits will be applied to that organization only and cannot be transferred or + shared between organizations. +

+
+ +
+

Want to start fresh?

+

+ Create a new organization first. You will have to revisit this link after creating the + organization to redeem the code. +

+ +
-
- {organizations?.map((org) => ( -
setSelectedOrg(org.slug)}> - -
- ))} +
+ {redeemCodeEnabled ? ( + organizations?.map((org) => ( + setSelectedOrg(org.slug)} + /> + )) + ) : ( + + )}
{selectedOrg && ( setSelectedOrg(null)} /> )} From 18cc6f8fe9654e0b682d4f6564adc838977d197e Mon Sep 17 00:00:00 2001 From: Alfred Sultson <11300079+sultson@users.noreply.github.com> Date: Mon, 9 Feb 2026 05:26:01 +0100 Subject: [PATCH 12/22] Replace Edge Functions legacy OpenAI template with two new AI SDK-based templates (#40904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Edge Functions templates update ## What is the current behavior? An OpenAI template using GPT-3 and the legacy completions API. ## What is the new behavior? Removed - OpenAI text completion template Added - Stream text with AI SDK template - Generate recipes with AI SDK template ## Additional context - CORS inline as examples do not support multiple files yet - Tested both by direct invocation and via `useChat` ## Summary by CodeRabbit * **New Features** * Added "Stream text with AI SDK" template for real-time streaming responses with CORS support and robust error handling. * Added "Generate recipes with AI SDK" template for producing validated, structured recipe outputs. * **Refactor** * Replaced the previous OpenAI text completion template with two new AI SDK–based templates to provide streaming and schema-driven content generation. --------- Co-authored-by: Kalleby Santos Co-authored-by: Joshen Lim --- .../Functions/Functions.templates.ts | 194 +++++++++++++++--- 1 file changed, 166 insertions(+), 28 deletions(-) diff --git a/apps/studio/components/interfaces/Functions/Functions.templates.ts b/apps/studio/components/interfaces/Functions/Functions.templates.ts index 4a984851471..fedfd558221 100644 --- a/apps/studio/components/interfaces/Functions/Functions.templates.ts +++ b/apps/studio/components/interfaces/Functions/Functions.templates.ts @@ -136,37 +136,175 @@ app.get(/slug/(.*)/, (req, res) => { app.listen(8000);`, }, { - value: 'openai-completion', - name: 'OpenAI Text Completion', - description: 'Generate text completions using OpenAI GPT-3', - content: `// Setup type definitions for built-in Supabase Runtime APIs -import "jsr:@supabase/functions-js/edge-runtime.d.ts" -import { OpenAI } from "npm:openai@4.8.0" + value: 'stream-text-with-ai-sdk', + name: 'Stream text with AI SDK', + description: 'Generate and stream text with Vercel AI SDK', + content: `/* + * Setup OPENAI_API_KEY secret to get started. + * For usage with useChat, point transport.api to this endpoint + * and include your publishable key as Authorization: Bearer in transport.headers. + */ -const openai = new OpenAI({ - apiKey: Deno.env.get('OPENAI_API_KEY') -}) +import { createOpenAI } from 'npm:@ai-sdk/openai'; +import { convertToModelMessages, streamText } from 'npm:ai'; -Deno.serve(async (req)=>{ - const { prompt } = await req.json() - const response = await openai.chat.completions.create({ - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: prompt - } - ] - }) - return new Response(JSON.stringify({ - text: response.choices[0].message.content - }), { - headers: { - 'Content-Type': 'application/json', - 'Connection': 'keep-alive' +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'authorization, content-type', + 'Access-Control-Max-Age': '3600', + Vary: 'Access-Control-Request-Headers', +}; + +const json = (status: number, body: unknown) => + new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + +class ClientError extends Error {} + +const openai = createOpenAI({ + apiKey: Deno.env.get('OPENAI_API_KEY'), +}); + +const SYSTEM_PROMPT = 'You are a helpful AI assistant.'; + +Deno.serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }); + } + + try { + const body = await req.json().catch(() => { + throw new ClientError('Invalid JSON payload'); + }) as { messages?: unknown; model?: unknown }; + + const { messages, model: modelName } = body; + + if (!Array.isArray(messages)) { + throw new ClientError('Request must include a messages array'); } - }) -})`, + + const normalizedMessages = await convertToModelMessages(messages); + + const model = openai( + typeof modelName === 'string' ? modelName : 'gpt-5.1-chat-latest', + ); + + const result = streamText({ + model, + messages: normalizedMessages, + system: SYSTEM_PROMPT, + }); + + return result.toUIMessageStreamResponse({ + headers: corsHeaders, + sendReasoning: true, + sendSources: true, + }); + } catch (err) { + if (err instanceof ClientError) { + return json(400, { error: err.message }); + } + + console.error('Assistant chat error:', err); + return json(500, { + error: 'Failed to process chat request', + details: err instanceof Error ? err.message : String(err), + }); + } +});`, + }, + { + value: 'generate-recipes-with-ai-sdk', + name: 'Generate recipes with AI SDK', + description: 'Generate structured cooking recipes with Vercel AI SDK', + content: `/* +* 1) Setup OPENAI_API_KEY secret to get started. +* 2) Call this endpoint with { prompt, model? } to generate a recipe object matching the schema below. +*/ + +import { createOpenAI } from 'npm:@ai-sdk/openai'; +import { generateText, Output } from "npm:ai"; +import { z } from 'npm:zod'; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'authorization, content-type', + 'Access-Control-Max-Age': '3600', + Vary: 'Access-Control-Request-Headers', +}; + +const json = (status: number, body: unknown) => + new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + +class ClientError extends Error {} + +const openai = createOpenAI({ + apiKey: Deno.env.get('OPENAI_API_KEY'), +}); + +const RecipeSchema = z.object({ + recipe: z.object({ + name: z.string(), + ingredients: z.array(z.string()), + steps: z.array(z.string()), + }), +}); + +const SYSTEM_PROMPT = + 'You are a recipe generator. Always return a structured recipe matching the given schema.'; + +Deno.serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }); + } + + try { + const body = await req.json().catch(() => { + throw new ClientError('Invalid JSON payload'); + }) as { + model?: unknown; + prompt?: unknown; + }; + + const { model: modelName, prompt } = body; + + if (typeof prompt !== 'string' || !prompt.trim()) { + throw new ClientError('Request must include a non-empty prompt string'); + } + + const model = openai( + typeof modelName === 'string' ? modelName : 'gpt-5.1-chat-latest', + ); + + const result = await generateText({ + model, + system: SYSTEM_PROMPT, + prompt, + output: Output.object({ + schema: RecipeSchema, + }), + }); + + return json(200, result.output); + } catch (err) { + if (err instanceof ClientError) { + return json(400, { error: err.message }); + } + + console.error('generateText error:', err); + return json(500, { + error: 'Failed to process generateText request', + details: err instanceof Error ? err.message : String(err), + }); + } +});`, }, { value: 'stripe-webhook', From 0628f2f07d70a152de63f678bcfe5aa1d70d178c Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 9 Feb 2026 16:04:39 +0800 Subject: [PATCH 13/22] [FE-2495] fix(studio): ReferenceRecordPeek breaking with JSONB ARRAY data (#42602) Issue: - When opening the peek record foreign key viewer, a full screen React error would happen Change: - Uses the preexisting formatters for the `` component To test: - Create two tables with a foreign key reference. I did `posts <- comments` - On your "posts" table, create a column with a jsonb array column and **add at least one item to the array** - On your "comments" table, open the foreign row peek and make sure no errors occur ## Summary by CodeRabbit * **Refactor** * Optimized grid component performance through improved column rendering and enhanced caching mechanisms. * Added specialized formatters for binary and JSON data types to improve display quality. * Improved memory efficiency and rendering speed, particularly when working with large datasets. * Streamlined cell rendering logic for more responsive data grids. --- .../formatter/ReferenceRecordPeek.tsx | 105 +++++++++--------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx b/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx index 0ac4073ff3a..6b9254f9127 100644 --- a/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx +++ b/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx @@ -1,7 +1,4 @@ import { PostgresTable } from '@supabase/postgres-meta' -import { Key } from 'lucide-react' -import DataGrid, { Column } from 'react-data-grid' - import { keepPreviousData } from '@tanstack/react-query' import { useParams } from 'common' import { COLUMN_MIN_WIDTH } from 'components/grid/constants' @@ -9,13 +6,20 @@ import { ESTIMATED_CHARACTER_PIXEL_WIDTH, getColumnDefaultWidth, } from 'components/grid/utils/gridColumns' -import { convertByteaToHex } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' +import { isArrayColumn, isBinaryColumn, isJsonColumn } from 'components/grid/utils/types' import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Key } from 'lucide-react' +import { useMemo } from 'react' +import DataGrid, { Column } from 'react-data-grid' +import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { BinaryFormatter } from './BinaryFormatter' +import { DefaultFormatter } from './DefaultFormatter' +import { JsonFormatter } from './JsonFormatter' + interface ReferenceRecordPeekProps { table: PostgresTable column: string @@ -44,59 +48,52 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee { placeholderData: keepPreviousData } ) - const primaryKeys = table.primary_keys.map((x) => x.name) + const primaryKeys = useMemo(() => table.primary_keys.map((x) => x.name), [table.primary_keys]) - const columns = (table?.columns ?? []).map((column) => { - const columnDefaultWidth = getColumnDefaultWidth({ - dataType: column.data_type, - format: column.format, - } as any) - const columnWidthBasedOnName = - (column.name.length + column.format.length) * ESTIMATED_CHARACTER_PIXEL_WIDTH - const columnWidth = - columnDefaultWidth < columnWidthBasedOnName ? columnWidthBasedOnName : columnDefaultWidth - const isPrimaryKey = primaryKeys.includes(column.name) + const columns = useMemo(() => { + return (table?.columns ?? []).map((column) => { + const columnDefaultWidth = getColumnDefaultWidth({ + dataType: column.data_type, + format: column.format, + } as any) + const columnWidthBasedOnName = + (column.name.length + column.format.length) * ESTIMATED_CHARACTER_PIXEL_WIDTH + const columnWidth = + columnDefaultWidth < columnWidthBasedOnName ? columnWidthBasedOnName : columnDefaultWidth + const isPrimaryKey = primaryKeys.includes(column.name) - const res: Column = { - key: column.name, - name: column.name, - resizable: false, - draggable: false, - sortable: false, - width: columnWidth, - minWidth: COLUMN_MIN_WIDTH, - headerCellClass: 'outline-none !shadow-none', - renderHeaderCell: () => ( -
- {isPrimaryKey && ( - - - - - Primary key - - )} - {column.name} - {column.format} -
- ), - renderCell: ({ column: col, row }) => { - const value = row[col.name as any] - const formattedValue = column.format === 'bytea' ? convertByteaToHex(value) : value - return ( -
= { + key: column.name, + name: column.name, + resizable: false, + draggable: false, + sortable: false, + width: columnWidth, + minWidth: COLUMN_MIN_WIDTH, + headerCellClass: 'outline-none !shadow-none', + renderHeaderCell: () => ( +
+ {isPrimaryKey && ( + + + + + Primary key + )} - > - {formattedValue === null ? 'NULL' : formattedValue} + {column.name} + {column.format}
- ) - }, - } - return res - }) + ), + renderCell: isBinaryColumn(column.data_type) + ? BinaryFormatter + : isJsonColumn(column.data_type) && !isArrayColumn(column.data_type) + ? JsonFormatter + : DefaultFormatter, + } + return res + }) + }, [table?.columns, primaryKeys]) return ( <> From ddc687bd64ada338bb43ef8d28524b5deddc1c83 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Feb 2026 16:30:02 +0800 Subject: [PATCH 14/22] Fix missing loader in table grid editor while rows are loading (#42605) ## Context Fix missing loader in table grid editor while the rows are loading - the loading UI was rendering, but its width was 0 hence not showing up ## Summary by CodeRabbit * **Style** * Improved grid layout styling to utilize full-width display configuration. * Enhanced loading skeleton container styling with better absolute positioning and spacing. * Refined empty state container layout for improved visual presentation. * **Refactor** * Removed unused internal dependencies. --- apps/studio/components/grid/SupabaseGrid.tsx | 2 +- apps/studio/components/grid/components/grid/Grid.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 8a0ae8429f5..13f492af43c 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -1,5 +1,5 @@ import { keepPreviousData, useQueryClient } from '@tanstack/react-query' -import { useFlag, useParams } from 'common' +import { useParams } from 'common' import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx index 4b7890caeae..54a9febe7cd 100644 --- a/apps/studio/components/grid/components/grid/Grid.tsx +++ b/apps/studio/components/grid/components/grid/Grid.tsx @@ -237,7 +237,7 @@ export const Grid = memo( {(rows ?? []).length === 0 && (
- {isLoading && !isDisabled && } + {isLoading && !isDisabled && ( + + )} {isError && } From 608481cfd7e153aa0b5cde2c8f5f8da2c3019f0e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Feb 2026 16:43:35 +0800 Subject: [PATCH 15/22] Prevent double click action in ReferenceRecordPeak (#42606) ## Context Tiny UX behaviour that we came across - just prevents the double click action when peeking a foreign row in the table editor Feels odd that double clicking the `ReferenceRecordPeek` opens the side panel to select a new foreign row to reference ## Summary by CodeRabbit * **Bug Fixes** * Enhanced double-click handling to prevent unintended actions in the foreign key selection popover. --- .../formatter/ForeignKeyFormatter.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx index 63c9b9722c5..bfa8f2cab28 100644 --- a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx +++ b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx @@ -1,16 +1,16 @@ import type { PostgresTable } from '@supabase/postgres-meta' -import { ArrowRight } from 'lucide-react' -import type { PropsWithChildren } from 'react' -import type { RenderCellProps } from 'react-data-grid' - import { convertByteaToHex } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { useTableQuery } from 'data/tables/table-retrieve-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { ArrowRight } from 'lucide-react' +import type { PropsWithChildren } from 'react' +import type { RenderCellProps } from 'react-data-grid' import { Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' + import type { SupaRow } from '../../types' import { NullValue } from '../common/NullValue' import { ReferenceRecordPeek } from './ReferenceRecordPeek' @@ -83,7 +83,14 @@ export const ForeignKeyFormatter = (props: Props) => { tooltip={{ content: { side: 'bottom', text: 'View referencing record' } }} /> - + { + e.preventDefault() + e.stopPropagation() + }} + > Date: Mon, 9 Feb 2026 16:56:23 +0800 Subject: [PATCH 16/22] Make edge functions page accessible on coming up (#42549) ## Context Related prior work was making edge functions page accessible while project is restoring - however the status of the project gets flipped to COMING_UP while it's restoring hence why the functions page was still inaccessible. ## Changes involved - Update `NavigationBar.utils` to have edge functions link to `/functions` irregardless of project status - (Unrelated) Disable (with tooltip) deploy new function CTA on edge functions page if project is not active image ## Summary by CodeRabbit * **New Features** * Deploy Edge Function button now disables and shows a tooltip when the project is inactive to make deployment status clear. * **Bug Fixes** * Edge Functions link now points consistently to the functions view regardless of project build state. * **Refactor** * Consolidated project-active check and reorganized related UI wiring to standardize inactive-state handling across settings and controls. --- .../DeployEdgeFunctionButton.tsx | 37 ++++++++++++++----- .../interfaces/Settings/Addons/Addons.tsx | 23 +++++++----- .../DatabaseSettings/ResetDbPassword.tsx | 8 ++-- .../Infrastructure/PauseProjectButton.tsx | 16 ++++---- .../Infrastructure/RestartServerButton.tsx | 18 +++++---- .../StorageSettings/CreateCredentialModal.tsx | 11 +++--- .../Storage/StorageSettings/S3Connection.tsx | 20 +++++----- .../NavigationBar/NavigationBar.utils.tsx | 5 +-- .../layouts/ProjectLayout/ProjectContext.tsx | 8 ---- apps/studio/hooks/misc/useSelectedProject.ts | 7 +++- 10 files changed, 84 insertions(+), 69 deletions(-) diff --git a/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton.tsx b/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton.tsx index c33b20001ad..06b36e34196 100644 --- a/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton.tsx +++ b/apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton.tsx @@ -1,37 +1,54 @@ -import { ChevronDown, Code, Terminal } from 'lucide-react' -import { parseAsString, useQueryState } from 'nuqs' -import { useRouter } from 'next/router' - import { useParams } from 'common' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { ChevronDown, Code, Terminal } from 'lucide-react' +import { useRouter } from 'next/router' +import { parseAsString, useQueryState } from 'nuqs' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AiIconAnimation, - Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from 'ui' -import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { useIsProjectActive } from '@/hooks/misc/useSelectedProject' export const DeployEdgeFunctionButton = () => { const router = useRouter() const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() + const snap = useAiAssistantStateSnapshot() const { openSidebar } = useSidebarManagerSnapshot() + const { mutate: sendEvent } = useSendEventMutation() const [, setCreateMethod] = useQueryState('create', parseAsString) + const isProjectActive = useIsProjectActive() + return ( - - + { diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index b2a9580b975..ccd34795724 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -1,26 +1,28 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ChevronDown } from 'lucide-react' -import { useRouter } from 'next/router' -import { useState } from 'react' -import { toast } from 'sonner' - import { useFlag } from 'common' -import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useProjectRestartMutation } from 'data/projects/project-restart-mutation' import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { + useIsAwsK8sCloudProvider, + useIsProjectActive, + useSelectedProjectQuery, +} from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' +import { ChevronDown } from 'lucide-react' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { toast } from 'sonner' import { Button, + cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - cn, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx index fd2855a3997..848faca105c 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx @@ -1,16 +1,14 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Plus } from 'lucide-react' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' - import { FormField } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' -import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useIsProjectActive } from 'hooks/misc/useSelectedProject' +import { Plus } from 'lucide-react' +import { useState } from 'react' +import { useForm } from 'react-hook-form' import { Button, Dialog, @@ -29,6 +27,7 @@ import { } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' interface CreateCredentialModalProps { visible: boolean diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index 4878a5878bd..afe09252182 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -1,14 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { AlertTitle } from '@ui/components/shadcn/ui/alert' -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { useParams } from 'common' -import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' @@ -17,19 +10,22 @@ import { useProjectStorageConfigQuery } from 'data/config/project-storage-config import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useIsProjectActive, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { - AlertDescription_Shadcn_, Alert_Shadcn_, + AlertDescription_Shadcn_, Button, Card, CardContent, CardFooter, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Form_Shadcn_, - Separator, Switch, Table, TableBody, @@ -52,6 +48,8 @@ import { PageSectionTitle, } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import * as z from 'zod' + import { CreateCredentialModal } from './CreateCredentialModal' import { RevokeCredentialModal } from './RevokeCredentialModal' import { StorageCredItem } from './StorageCredItem' diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 6ad7eb1e82a..5914bd62821 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -1,5 +1,3 @@ -import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react' - import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar' import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils' import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils' @@ -9,6 +7,7 @@ import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' import type { Project } from 'data/projects/project-detail-query' import { Auth, Database, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' +import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react' export const generateToolRoutes = (ref?: string, project?: Project, features?: {}): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP @@ -102,7 +101,7 @@ export const generateProductRoutes = ( key: 'functions', label: 'Edge Functions', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/functions`), + link: ref && `/project/${ref}/functions`, }, ] : []), diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx index b17237bc37e..1ac054e7c8e 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -1,7 +1,4 @@ import { PropsWithChildren } from 'react' - -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { PROJECT_STATUS } from 'lib/constants' import { DatabaseSelectorStateContextProvider } from 'state/database-selector' import { RoleImpersonationStateContextProvider } from 'state/role-impersonation-state' import { StorageExplorerStateContextProvider } from 'state/storage-explorer' @@ -30,8 +27,3 @@ export const ProjectContextProvider = ({ ) } - -export const useIsProjectActive = () => { - const { data: project } = useSelectedProjectQuery() - return project?.status === PROJECT_STATUS.ACTIVE_HEALTHY -} diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index 9829532f968..48e208b939b 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -1,6 +1,6 @@ import { useParams } from 'common' import { useProjectDetailQuery } from 'data/projects/project-detail-query' -import { PROVIDERS } from 'lib/constants' +import { PROJECT_STATUS, PROVIDERS } from 'lib/constants' export function useSelectedProjectQuery({ enabled = true } = {}) { const { ref } = useParams() @@ -52,3 +52,8 @@ export const useIsOrioleDbInAws = () => { project?.dbVersion?.endsWith('orioledb') && project?.cloud_provider === PROVIDERS.AWS.id return isOrioleDbInAws } + +export const useIsProjectActive = () => { + const { data: project } = useSelectedProjectQuery() + return project?.status === PROJECT_STATUS.ACTIVE_HEALTHY +} From 839b36737542a2fa49235ec462400d4d64c3dc84 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Feb 2026 17:33:02 +0800 Subject: [PATCH 17/22] chore(studio): Consolidate incident banner + maintenance banner, make incident banner dismissible (#42551) ## Context PR mainly adds a change to allow users to dismiss an incident banner Am also taking this time to consolidate `IncidentBanner` and `MaintenanceBanner` into a single component `StatusPageBanner` so that the logic for rendering either component is more clearer. ## Intended behaviour (should be covered by e2e tests) - Incident banner shows if there are incidents with impact !== 'none' - Incident banner still takes precedence over Maintanence banner (if there's both incident + maintenence events) - Dismissing the incident banner will subsequently show the maintenance banner if the maintenance hasn't been dismissed before (just to make sure that the user doesn't miss any comms) - Incident banner will not be dismissible if there's an override (via feature flag or env var) ## Summary by CodeRabbit * **New Features** * Unified status page banner showing incident or maintenance notifications, with dismissible banners that persist across sessions; incident notices take precedence over maintenance. * **Refactor** * Replaced separate incident and maintenance banners with a single consolidated StatusPageBanner component. * **Tests** * Added end-to-end tests covering banner visibility, precedence, and dismissal behavior. --- .../interfaces/App/AppBannerWrapper.tsx | 17 +- .../layouts/AppLayout/IncidentBanner.tsx | 20 -- .../layouts/AppLayout/MaintenanceBanner.tsx | 36 ---- .../layouts/AppLayout/StatusPageBanner.tsx | 68 +++++++ .../data/platform/incident-status-query.ts | 5 +- .../features/status-page-banner.spec.ts | 174 ++++++++++++++++++ packages/common/constants/local-storage.ts | 1 + 7 files changed, 248 insertions(+), 73 deletions(-) delete mode 100644 apps/studio/components/layouts/AppLayout/IncidentBanner.tsx delete mode 100644 apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx create mode 100644 apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx create mode 100644 e2e/studio/features/status-page-banner.spec.ts diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx index de62e8abea1..215efbfa17d 100644 --- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -1,32 +1,19 @@ import { useFlag } from 'common' import { ClockSkewBanner } from 'components/layouts/AppLayout/ClockSkewBanner' -import { IncidentBanner } from 'components/layouts/AppLayout/IncidentBanner' import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner' +import { StatusPageBanner } from 'components/layouts/AppLayout/StatusPageBanner' import { PropsWithChildren } from 'react' import { OrganizationResourceBanner } from '../Organization/HeaderBanner' -import { MaintenanceBanner } from '@/components/layouts/AppLayout/MaintenanceBanner' -import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { - const { data: allStatusPageEvents } = useIncidentStatusQuery() - const { maintenanceEvents = [], incidents = [] } = allStatusPageEvents ?? {} - - // Only show incident banner for incidents with real impact (not "none") - const hasBannerWorthyIncidents = incidents.some((incident) => incident.impact !== 'none') - const ongoingIncident = - useFlag('ongoingIncident') || - process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true' || - hasBannerWorthyIncidents - const ongoingMaintenance = maintenanceEvents.length > 0 - const showNoticeBanner = useFlag('showNoticeBanner') const clockSkewBanner = useFlag('clockSkewBanner') return (
- {ongoingIncident ? : ongoingMaintenance ? : null} + {showNoticeBanner && } {clockSkewBanner && } diff --git a/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx b/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx deleted file mode 100644 index 5f5830ef2e1..00000000000 --- a/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner' -import { InlineLink } from 'components/ui/InlineLink' - -/** - * Used to display ongoing incidents - */ -export const IncidentBanner = () => { - return ( - - Follow the status page for - updates - - } - /> - ) -} diff --git a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx b/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx deleted file mode 100644 index 4c5610b36b6..00000000000 --- a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { LOCAL_STORAGE_KEYS } from 'common' -import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner' -import { InlineLink } from 'components/ui/InlineLink' - -import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' - -/** - * Used to display ongoing maintenance - */ -export function MaintenanceBanner() { - const { data: allStatusPageEvents } = useIncidentStatusQuery() - const { maintenanceEvents = [] } = allStatusPageEvents ?? {} - const currentEventId = maintenanceEvents[0]?.id ?? '' - - const [dismissed, setDismissed] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(currentEventId), - false - ) - - if (dismissed) return null - - return ( - - Follow the status page for - updates - - } - onDismiss={() => setDismissed(true)} - /> - ) -} diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx new file mode 100644 index 00000000000..b5ffe723f8c --- /dev/null +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx @@ -0,0 +1,68 @@ +import { LOCAL_STORAGE_KEYS, useFlag } from 'common' +import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner' +import { InlineLink } from 'components/ui/InlineLink' + +import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' + +const BANNER_DESCRIPTION = ( + <> + Follow the status page for updates + +) + +/** + * Used to display ongoing incidents or maintenances + */ +export const StatusPageBanner = () => { + const { data: allStatusPageEvents } = useIncidentStatusQuery() + const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {} + + // Only show incident banner for incidents with real impact (not "none") + const highImpactIncident = incidents.find((incident) => incident.impact !== 'none') + const incidentEventId = highImpactIncident?.id ?? '' + + const showIncidentBannerOverride = + useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true' + + const ongoingMaintenance = maintenanceEvents.length > 0 + const maintenanceEventId = maintenanceEvents[0]?.id ?? '' + + const [dismissedIncident, setDismissedIncident] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED(incidentEventId), + false + ) + + const [dismissedMaintenance, setDismissedMaintenance] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(maintenanceEventId), + false + ) + + if (showIncidentBannerOverride || (highImpactIncident && !dismissedIncident)) { + return ( + setDismissedIncident(true) + } + /> + ) + } + + if (ongoingMaintenance && !dismissedMaintenance) { + return ( + setDismissedMaintenance(true)} + /> + ) + } + + return null +} diff --git a/apps/studio/data/platform/incident-status-query.ts b/apps/studio/data/platform/incident-status-query.ts index a6db924e096..7c767fc1cb5 100644 --- a/apps/studio/data/platform/incident-status-query.ts +++ b/apps/studio/data/platform/incident-status-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import type { IncidentInfo } from 'lib/api/incident-status' -import { BASE_PATH, IS_PLATFORM } from 'lib/constants' +import { BASE_PATH, IS_PLATFORM, IS_TEST_ENV } from 'lib/constants' import { partition } from 'lodash' import { UseCustomQueryOptions } from 'types' @@ -42,5 +42,6 @@ export const useIncidentStatusQuery = ( queryFn: ({ signal }) => getIncidentStatus(signal), staleTime: 1000 * 60 * 5, // 5 minutes to match API cache ...options, - enabled: IS_PLATFORM && (options.enabled ?? true), + // Enable in platform mode, or in test environment for E2E testing + enabled: (IS_PLATFORM || IS_TEST_ENV) && (options.enabled ?? true), }) diff --git a/e2e/studio/features/status-page-banner.spec.ts b/e2e/studio/features/status-page-banner.spec.ts new file mode 100644 index 00000000000..a1c504e1dfd --- /dev/null +++ b/e2e/studio/features/status-page-banner.spec.ts @@ -0,0 +1,174 @@ +import { expect } from '@playwright/test' + +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' + +// Mock incident data - impact is "major" (not "none" or "maintenance") +const mockIncident = { + id: 'incident-123', + name: 'Test Incident', + status: 'investigating', + impact: 'major', + active_since: new Date().toISOString(), +} + +// Mock incident with "none" impact - should NOT trigger the incident banner +const mockIncidentNoneImpact = { + id: 'incident-none-456', + name: 'No Impact Incident', + status: 'investigating', + impact: 'none', + active_since: new Date().toISOString(), +} + +// Mock maintenance event - impact is "maintenance" +const mockMaintenance = { + id: 'maintenance-789', + name: 'Scheduled Maintenance', + status: 'in_progress', + impact: 'maintenance', + active_since: new Date().toISOString(), +} + +test.describe('StatusPageBanner', () => { + test.describe.configure({ mode: 'serial' }) + + test.beforeEach(async ({ page }) => { + // Clear any dismissed banner state from localStorage before each test + await page.addInitScript(() => { + // Clear all incident/maintenance banner dismissals and test overrides + for (const key of Object.keys(localStorage)) { + if ( + key.startsWith('incident-banner-dismissed-') || + key.startsWith('maintenance-banner-dismissed-') + ) { + localStorage.removeItem(key) + } + } + }) + }) + + test('incident banner shows for incidents with impact not equal to "none"', async ({ + page, + ref, + }) => { + // Mock the incident status API to return an incident with major impact + await page.route('**/api/incident-status', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([mockIncident]), + }) + }) + + await page.goto(toUrl(`/project/${ref}`)) + + // Wait for the incident banner to be visible + const incidentBanner = page.getByText('We are investigating a technical issue') + await expect(incidentBanner).toBeVisible({ timeout: 15000 }) + + // Verify the dismiss button IS present for real incidents + const dismissButton = page.getByRole('button', { name: 'Dismiss banner' }) + await expect(dismissButton).toBeVisible() + }) + + test('incident banner does NOT show for incidents with impact "none"', async ({ page, ref }) => { + // Mock the incident status API to return an incident with "none" impact + await page.route('**/api/incident-status', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([mockIncidentNoneImpact]), + }) + }) + + await page.goto(toUrl(`/project/${ref}`)) + + // Wait for page to load + await expect(page.getByRole('heading', { level: 1 })).toBeVisible({ timeout: 15000 }) + + // Verify the incident banner is NOT visible + const incidentBanner = page.getByText('We are investigating a technical issue') + await expect(incidentBanner).not.toBeVisible() + }) + + test('incident banner takes precedence over maintenance banner', async ({ page, ref }) => { + // Mock the incident status API to return both an incident and a maintenance event + await page.route('**/api/incident-status', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([mockIncident, mockMaintenance]), + }) + }) + + await page.goto(toUrl(`/project/${ref}`)) + + // Wait for the incident banner to be visible + const incidentBanner = page.getByText('We are investigating a technical issue') + await expect(incidentBanner).toBeVisible({ timeout: 15000 }) + + // Verify the maintenance banner is NOT visible (incident takes precedence) + const maintenanceBanner = page.getByText('Scheduled maintenance is in progress') + await expect(maintenanceBanner).not.toBeVisible() + }) + + test('maintenance banner shows after dismissing incident banner when both exist', async ({ + page, + ref, + }) => { + // Mock the incident status API to return both an incident and a maintenance event + await page.route('**/api/incident-status', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([mockIncident, mockMaintenance]), + }) + }) + + await page.goto(toUrl(`/project/${ref}`)) + + // Wait for the incident banner to be visible + const incidentBanner = page.getByText('We are investigating a technical issue') + await expect(incidentBanner).toBeVisible({ timeout: 15000 }) + + // Click the dismiss button to dismiss the incident banner + const dismissButton = page.getByRole('button', { name: 'Dismiss banner' }) + await expect(dismissButton).toBeVisible() + await dismissButton.click() + + // Wait for the incident banner to disappear + await expect(incidentBanner).not.toBeVisible({ timeout: 5000 }) + + // Verify the maintenance banner is now visible + const maintenanceBanner = page.getByText('Scheduled maintenance is in progress') + await expect(maintenanceBanner).toBeVisible({ timeout: 5000 }) + }) + + test('maintenance banner is dismissible', async ({ page, ref }) => { + // Mock the incident status API to return only a maintenance event + await page.route('**/api/incident-status', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([mockMaintenance]), + }) + }) + + await page.goto(toUrl(`/project/${ref}`)) + + // Wait for the maintenance banner to be visible + const maintenanceBanner = page.getByText('Scheduled maintenance is in progress') + await expect(maintenanceBanner).toBeVisible({ timeout: 15000 }) + + // Verify the dismiss button is present + const dismissButton = page.getByRole('button', { name: 'Dismiss banner' }) + await expect(dismissButton).toBeVisible() + + // Dismiss the maintenance banner + await dismissButton.click() + + // Verify the maintenance banner is no longer visible + await expect(maintenanceBanner).not.toBeVisible({ timeout: 5000 }) + }) +}) diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 44e56284601..6a6aeb6518b 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -9,6 +9,7 @@ export const LOCAL_STORAGE_KEYS = { PROJECTS_VIEW: 'projects-view', FEEDBACK_WIDGET_CONTENT: 'feedback-widget-content', FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot', + INCIDENT_BANNER_DISMISSED: (id: string) => `incident-banner-dismissed-${id}`, MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`, UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel', From 7f13c269ac81f19408bdfce0b8fde126a4fd89b1 Mon Sep 17 00:00:00 2001 From: Prashant Sridharan <914007+CoolAssPuppy@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:16:31 +0000 Subject: [PATCH 18/22] Added Eric Simons to webinar promo (#42608) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Quick change to the webinar metadata to highlight that the Bolt CEO will be co-hosting with us. ## Summary by CodeRabbit * **Documentation** * Updated event marketing materials for the 2026 Enterprise Innovation webinar with enhanced branding and messaging featuring Bolt.new. * **Style** * Code formatting improvements. --- .../_events/2026-02-25-enterprise-innovation-with-bolt.mdx | 6 +++--- packages/common/feature-flags.tsx | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/www/_events/2026-02-25-enterprise-innovation-with-bolt.mdx b/apps/www/_events/2026-02-25-enterprise-innovation-with-bolt.mdx index 52efe1880ae..54953aafef1 100644 --- a/apps/www/_events/2026-02-25-enterprise-innovation-with-bolt.mdx +++ b/apps/www/_events/2026-02-25-enterprise-innovation-with-bolt.mdx @@ -2,9 +2,9 @@ title: 'Vibe Coding, Done Right: AI Development in Production' meta_title: 'Vibe Coding, Done Right: AI Development in Production' subtitle: >- - Learn how enterprise innovation teams are using AI coding tools like Bolt to build real applications on Supabase + Join Bolt CEO Eric Simons and learn how enterprise innovation teams are using AI coding tools to build real applications on Supabase meta_description: >- - Learn how enterprise teams use AI coding tools like Bolt to build production apps. Real customer story, governance models, and security practices. + Join Bolt CEO Eric Simons and learn how enterprise teams use AI coding tools to build production apps. Real customer story, governance models, and security practices. type: webinar onDemand: false date: '2026-02-25T07:00:00.000-08:00' @@ -23,7 +23,7 @@ speakers: 'chris_caruso,eric_simons' Something strange is happening in large enterprises. Non-technical employees are building production software. CEOs are shipping features. And companies are canceling SaaS contracts worth millions. -In this webinar, you will learn how enterprise innovation teams are using AI coding tools like Bolt to build real applications on Supabase. We will show you how to rapidly prototype internal tools, how to work within the constraints of an existing design system, and how to empower non/semi-technical teams to build safely. +In this webinar, join Bolt.new CEO Eric Simons and learn how enterprise innovation teams are using AI coding tools like Bolt to build real applications on Supabase. We will show you how to rapidly prototype internal tools, how to work within the constraints of an existing design system, and how to empower non/semi-technical teams to build safely. ## Key Takeaways diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index cbaaba96c9c..08382c3a507 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -165,9 +165,7 @@ export const FeatureFlagProvider = ({ const isLocalDev = process.env.NODE_ENV === 'development' - const safeParse = ( - value: string | undefined - ): Record => { + const safeParse = (value: string | undefined): Record => { if (!value) return {} try { return JSON.parse(value) From 8ffdcfc145d6731804d623ba028deeba68778e4d Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:32:55 +0000 Subject: [PATCH 19/22] feat(studio): scoped pat ui (#38170) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Introduces UI for Scoped PAT's. ## Additional context WIP. Testing endpoints. ## Summary by CodeRabbit * **New Features** * Scoped access tokens: create, view, delete, and manage granular resource/action permissions with custom expiry options and scoped creation flows. * New scoped token UI: modal/sheet creation flow, resource access selectors, permissions editor, and a scoped token list. * **UI Improvements** * Tabbed Classic vs Scoped tokens layout, refreshed tokens page, improved forms, search/filtering, and table tweaks. * **Bug Fixes / Maintenance** * New data hooks and utilities for scoped tokens and token handling. * **Removed** * Legacy account settings layout and legacy token banner. --- .../AccessTokens/AccessToken.constants.ts | 75 ++++ .../AccessTokens/AccessToken.schemas.ts | 23 ++ .../Account/AccessTokens/AccessToken.types.ts | 25 ++ .../Account/AccessTokens/AccessToken.utils.ts | 148 ++++++++ .../Account/AccessTokens/AccessTokenList.tsx | 271 +++------------ .../AccessTokenNewBanner.tsx | 67 ++++ .../AccessTokenNewBanner/PermissionList.tsx | 28 ++ .../TokenPermissionSection.tsx | 55 +++ .../AccessTokenTable/RowLoading.tsx | 22 ++ .../AccessTokenTable/TableContainer.tsx | 44 +++ .../AccessTokenTable/TokenCells.tsx | 62 ++++ .../AccessTokens/AccessTokens.constants.ts | 31 -- .../NewTokenButton.tsx} | 9 +- .../NewTokenDialog.tsx} | 34 +- .../NewAccessTokenButton.test.tsx | 132 -------- .../Account/AccessTokens/NewTokenBanner.tsx | 48 --- .../AccessTokens/Scoped/Form/BasicInfo.tsx | 144 ++++++++ .../PermissionResourceSelector.tsx | 89 +++++ .../Scoped/Form/Permissions/Permissions.tsx | 172 ++++++++++ .../Form/Permissions/Permissions.types.ts | 33 ++ .../Form/Permissions/Permissions.utils.ts | 48 +++ .../Form/ResourceAccess/ResourceAccess.tsx | 115 +++++++ .../ResourceAccess/ResourceAccess.types.ts | 30 ++ .../ResourceAccess/ResourceMultiSelector.tsx | 68 ++++ .../Form/ResourceAccess/ResourceOption.tsx | 42 +++ .../Scoped/NewScopedTokenButton.tsx | 33 ++ .../Scoped/NewScopedTokenSheet.tsx | 320 ++++++++++++++++++ .../AccessTokens/Scoped/ScopedTokenList.tsx | 187 ++++++++++ .../AccessTokens/Scoped/ViewTokenSheet.tsx | 304 +++++++++++++++++ .../hooks/useGroupedPermissions.ts | 33 ++ .../hooks/useOrgAndProjectData.ts | 29 ++ .../AccessTokens/AccessTokensLayout.tsx | 72 ++++ .../layouts/AccountLayout/AccountLayout.tsx | 3 +- .../AccountLayout/AccountSettingsLayout.tsx | 58 ---- apps/studio/data/access-tokens/keys.ts | 1 + apps/studio/data/scoped-access-tokens/keys.ts | 4 + .../scoped-access-token-create-mutation.ts | 55 +++ .../scoped-access-token-query.ts | 60 ++++ .../scoped-access-tokens-delete-mutation.ts | 50 +++ apps/studio/package.json | 2 +- apps/studio/pages/account/tokens.tsx | 100 +++--- apps/studio/pages/account/tokens/scoped.tsx | 88 +++++ pnpm-lock.yaml | 10 +- 43 files changed, 2656 insertions(+), 568 deletions(-) create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessToken.schemas.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessToken.types.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessToken.utils.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/PermissionList.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/TokenPermissionSection.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TokenCells.tsx delete mode 100644 apps/studio/components/interfaces/Account/AccessTokens/AccessTokens.constants.ts rename apps/studio/components/interfaces/Account/AccessTokens/{NewAccessTokenButton.tsx => Classic/NewTokenButton.tsx} (84%) rename apps/studio/components/interfaces/Account/AccessTokens/{NewAccessTokenDialog.tsx => Classic/NewTokenDialog.tsx} (89%) delete mode 100644 apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.test.tsx delete mode 100644 apps/studio/components/interfaces/Account/AccessTokens/NewTokenBanner.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/BasicInfo.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/PermissionResourceSelector.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.utils.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.types.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceMultiSelector.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceOption.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/hooks/useGroupedPermissions.ts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData.ts create mode 100644 apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx delete mode 100644 apps/studio/components/layouts/AccountLayout/AccountSettingsLayout.tsx create mode 100644 apps/studio/data/scoped-access-tokens/keys.ts create mode 100644 apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts create mode 100644 apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts create mode 100644 apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts create mode 100644 apps/studio/pages/account/tokens/scoped.tsx diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts new file mode 100644 index 00000000000..75e1accca3f --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts @@ -0,0 +1,75 @@ +import { components } from 'api-types' +import { permissions } from '@supabase/shared-types' + +export type ScopedAccessTokenPermission = + components['schemas']['CreateScopedAccessTokenBody']['permissions'][number] + +export const NON_EXPIRING_TOKEN_VALUE = 'never' +export const CUSTOM_EXPIRY_VALUE = 'custom' + +export const EXPIRES_AT_OPTIONS = { + hour: { value: 'hour', label: '1 hour' }, + day: { value: 'day', label: '1 day' }, + week: { value: 'week', label: '7 days' }, + month: { value: 'month', label: '30 days' }, + never: { value: NON_EXPIRING_TOKEN_VALUE, label: 'Never' }, + custom: { value: CUSTOM_EXPIRY_VALUE, label: 'Custom' }, +} as const + +const FGA = permissions.FgaPermissions + +const getAction = (key: string): string => { + if (key.endsWith('_READ')) return 'read' + if (key.endsWith('_WRITE')) return 'write' + if (key.endsWith('_CREATE')) return 'create' + if (key.endsWith('_DELETE')) return 'delete' + return 'read' +} + +const getResource = (key: string): string => { + return key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase() +} + +const buildPermissionList = () => { + const list: Array<{ + scope: string + resource: string + action: string + id: string + title: string + }> = [] + + for (const [scope, scopePerms] of Object.entries(FGA)) { + for (const [key, perm] of Object.entries(scopePerms)) { + list.push({ + scope: scope.toLowerCase(), + resource: getResource(key), + action: getAction(key), + id: perm.id, + title: perm.title, + }) + } + } + + return list +} + +export const PERMISSION_LIST = buildPermissionList() + +export const ACCESS_TOKEN_RESOURCES = (() => { + const resourceMap = new Map() + + for (const p of PERMISSION_LIST) { + const key = `${p.scope}:${p.resource}` + if (!resourceMap.has(key)) { + const cleanTitle = p.title.replace(/^(Read|Manage|Create|Delete)\s+/i, '') + resourceMap.set(key, { resource: key, title: cleanTitle, actions: [] }) + } + const entry = resourceMap.get(key)! + if (!entry.actions.includes(p.action)) { + entry.actions.push(p.action) + } + } + + return Array.from(resourceMap.values()) +})() diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.schemas.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.schemas.ts new file mode 100644 index 00000000000..da0604f0633 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.schemas.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +export const PermissionRowSchema = z.object({ + resource: z.string().min(1, 'Please select a resource'), + actions: z.array(z.string()).min(1, 'Please select at least one action'), +}) + +export const TokenSchema = z + .object({ + tokenName: z.string().min(1, 'Please enter a name for the token'), + expiresAt: z.preprocess((val) => (val === 'never' ? undefined : val), z.string().optional()), + customExpiryDate: z.string().optional(), + resourceAccess: z.enum(['all-orgs', 'selected-orgs', 'selected-projects']), + selectedOrganizations: z.array(z.string()).optional(), + selectedProjects: z.array(z.string()).optional(), + permissionRows: z.array(PermissionRowSchema).min(1, 'Please configure at least one permission'), + }) + .refine((data) => !(data.expiresAt === 'custom' && !data.customExpiryDate), { + message: 'Please select a custom expiry date', + path: ['expiresAt'], + }) + +export type TokenFormValues = z.infer diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.types.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.types.ts new file mode 100644 index 00000000000..403fe2c96eb --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.types.ts @@ -0,0 +1,25 @@ +export const ACCESS_TOKEN_SORT_VALUES = [ + 'created_at:asc', + 'created_at:desc', + 'last_used_at:asc', + 'last_used_at:desc', + 'expires_at:asc', + 'expires_at:desc', +] as const + +export type AccessTokenSort = (typeof ACCESS_TOKEN_SORT_VALUES)[number] +export type AccessTokenSortColumn = AccessTokenSort extends `${infer Column}:${string}` + ? Column + : never +export type AccessTokenSortOrder = AccessTokenSort extends `${string}:${infer Order}` + ? Order + : never + +export interface BaseToken { + id: string | number + name: string + token_alias: string + created_at: string + last_used_at?: string | null + expires_at?: string | null +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.utils.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.utils.ts new file mode 100644 index 00000000000..20b573290ca --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.utils.ts @@ -0,0 +1,148 @@ +import dayjs from 'dayjs' +import { + AccessTokenSort, + AccessTokenSortColumn, + AccessTokenSortOrder, + BaseToken, +} from './AccessToken.types' +import { PERMISSION_LIST, ScopedAccessTokenPermission } from './AccessToken.constants' + +export const handleSortChange = ( + currentSort: AccessTokenSort, + column: AccessTokenSortColumn, + setSort: (sort: AccessTokenSort) => void +) => { + const [currentCol, currentOrder] = currentSort.split(':') as [ + AccessTokenSortColumn, + AccessTokenSortOrder, + ] + if (currentCol === column) { + if (currentOrder === 'asc') { + setSort(`${column}:desc` as AccessTokenSort) + } else { + setSort('created_at:desc') + } + } else { + setSort(`${column}:asc` as AccessTokenSort) + } +} + +export const filterAndSortTokens = ( + tokens: T[] | undefined, + searchString: string, + sort: AccessTokenSort +): T[] | undefined => { + const filtered = !searchString + ? tokens + : tokens?.filter((token) => token.name.toLowerCase().includes(searchString.toLowerCase())) + + if (!filtered) return filtered + + const [sortCol, sortOrder] = sort.split(':') as [AccessTokenSortColumn, AccessTokenSortOrder] + const orderMultiplier = sortOrder === 'asc' ? 1 : -1 + + return [...filtered].sort((a, b) => { + if (sortCol === 'created_at') { + return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier + } + if (sortCol === 'last_used_at') { + if (!a.last_used_at && !b.last_used_at) return 0 + if (!a.last_used_at) return 1 + if (!b.last_used_at) return -1 + return ( + (new Date(a.last_used_at).getTime() - new Date(b.last_used_at).getTime()) * orderMultiplier + ) + } + if (sortCol === 'expires_at') { + if (!a.expires_at && !b.expires_at) return 0 + if (!a.expires_at) return 1 + if (!b.expires_at) return -1 + return (new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) * orderMultiplier + } + return 0 + }) +} + +export const mapPermissionToFGA = ( + resourceKey: string, + action: string +): ScopedAccessTokenPermission[] => { + const [scope, resource] = resourceKey.split(':') + const match = PERMISSION_LIST.find( + (p) => p.scope === scope && p.resource === resource && p.action === action + ) + return match ? [match.id as ScopedAccessTokenPermission] : [] +} + +// [kemal]: Not sure how efficient this will be, but it should get permissions from shared types and transform them whenever @supabase/shared-types updates. +export const getResourcePermissions = ( + resourceKey: string +): Record => { + const [scope, resource] = resourceKey.split(':') + const result: Record = { 'no access': [] } + + PERMISSION_LIST.filter((p) => p.scope === scope && p.resource === resource).forEach((p) => { + result[p.action] = [p.id as ScopedAccessTokenPermission] + }) + + if (result['read'] && result['write']) { + result['read-write'] = [...result['read'], ...result['write']] + } + + return result +} + +export const getRealAccess = (resource: string, tokenPermissions: string[]) => { + const resourcePermissions = getResourcePermissions(resource) + const actionTypes = ['read', 'write', 'create', 'delete'] as const + const grantedActions = actionTypes.filter((action) => + resourcePermissions[action]?.some((p) => tokenPermissions.includes(p)) + ) + + if (grantedActions.length === 0) { + return 'no access' + } + + if (grantedActions.length === 1) { + return grantedActions[0] + } + + if ( + grantedActions.length === 2 && + grantedActions[0] === 'read' && + grantedActions[1] === 'write' + ) { + return 'read-write' + } + + return grantedActions.join('-') +} + +export const formatAccessText = (action: string): string => { + switch (action) { + case 'no access': + return 'No access' + default: + return action + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join('-') + } +} + +export const getExpirationDate = (key: string): string | undefined => { + switch (key) { + case 'hour': + return dayjs().add(1, 'hours').toISOString() + case 'day': + return dayjs().add(1, 'day').toISOString() + case 'week': + return dayjs().add(7, 'days').toISOString() + case 'month': + return dayjs().add(30, 'days').toISOString() + case 'never': + return undefined + default: + return undefined + } +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx index 1a0e8f30dbf..d6433c3ff86 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx @@ -1,100 +1,29 @@ import AlertError from 'components/ui/AlertError' import { useAccessTokenDeleteMutation } from 'data/access-tokens/access-tokens-delete-mutation' import { AccessToken, useAccessTokensQuery } from 'data/access-tokens/access-tokens-query' -import dayjs from 'dayjs' import { MoreVertical, Trash } from 'lucide-react' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { Button, - Card, - CardContent, - cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Skeleton, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableHeadSort, - TableRow, -} from 'ui/src/components/shadcn/ui/table' -import { TimestampInfo } from 'ui-patterns/TimestampInfo' +import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table' import { parseAsStringLiteral, useQueryState } from 'nuqs' -const ACCESS_TOKEN_SORT_VALUES = [ - 'created_at:asc', - 'created_at:desc', - 'last_used_at:asc', - 'last_used_at:desc', - 'expires_at:asc', - 'expires_at:desc', -] as const - -type AccessTokenSort = (typeof ACCESS_TOKEN_SORT_VALUES)[number] -type AccessTokenSortColumn = AccessTokenSort extends `${infer Column}:${string}` ? Column : unknown -type AccessTokenSortOrder = AccessTokenSort extends `${string}:${infer Order}` ? Order : unknown - -const RowLoading = () => ( - - - - - - - - - - - - - - - - - -) - -const tableHeaderClass = 'text-left font-mono uppercase text-xs text-foreground-lighter py-2' - -interface TableContainerProps { - children: React.ReactNode - sort: AccessTokenSort - onSortChange: (column: AccessTokenSortColumn) => void -} - -const TableContainer = ({ children, sort, onSortChange }: TableContainerProps) => ( - - - - - - Token - - - Last used - - - - - Expires - - - - - - {children} -
-
-
-) +import { + ACCESS_TOKEN_SORT_VALUES, + AccessTokenSort, + AccessTokenSortColumn, +} from './AccessToken.types' +import { handleSortChange, filterAndSortTokens } from './AccessToken.utils' +import { TableContainer } from './AccessTokenTable/TableContainer' +import { RowLoading } from './AccessTokenTable/RowLoading' +import { TokenNameCell, LastUsedCell, ExpiresCell } from './AccessTokenTable/TokenCells' export interface AccessTokenListProps { searchString?: string @@ -122,70 +51,20 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo }, }) - const handleSortChange = (column: AccessTokenSortColumn) => { - const [currentCol, currentOrder] = sort.split(':') as [ - AccessTokenSortColumn, - AccessTokenSortOrder, - ] - if (currentCol === column) { - if (currentOrder === 'asc') { - setSort(`${column}:desc` as AccessTokenSort) - } else { - setSort('created_at:desc') - } - } else { - setSort(`${column}:asc` as AccessTokenSort) - } + const onSortChange = (column: AccessTokenSortColumn) => { + handleSortChange(sort, column, setSort) } - const onDeleteToken = async (tokenId: number) => { - deleteToken({ id: tokenId }) - } - - const filteredTokens = useMemo(() => { - const filtered = !searchString - ? tokens - : tokens?.filter((token) => { - return token.name.toLowerCase().includes(searchString.toLowerCase()) - }) - - if (!filtered) return filtered - - const [sortCol, sortOrder] = sort.split(':') as [AccessTokenSortColumn, AccessTokenSortOrder] - const orderMultiplier = sortOrder === 'asc' ? 1 : -1 - - return [...filtered].sort((a, b) => { - if (sortCol === 'created_at') { - return ( - (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier - ) - } - if (sortCol === 'last_used_at') { - if (!a.last_used_at && !b.last_used_at) return 0 - if (!a.last_used_at) return 1 - if (!b.last_used_at) return -1 - return ( - (new Date(a.last_used_at).getTime() - new Date(b.last_used_at).getTime()) * - orderMultiplier - ) - } - if (sortCol === 'expires_at') { - if (!a.expires_at && !b.expires_at) return 0 - if (!a.expires_at) return 1 - if (!b.expires_at) return -1 - return ( - (new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) * orderMultiplier - ) - } - return 0 - }) - }, [tokens, searchString, sort]) + const filteredTokens = useMemo( + () => filterAndSortTokens(tokens, searchString, sort), + [tokens, searchString, sort] + ) const empty = filteredTokens?.length === 0 && !isLoading if (isError) { return ( - + + @@ -210,7 +89,7 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo if (empty) { return ( - +

No access tokens found

@@ -225,93 +104,51 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo return ( <> - - {filteredTokens?.map((x) => { - return ( - - -

- {x.name} -

-

- {x.token_alias} -

-
- - {x.last_used_at ? ( - - ) : ( -

Never used

- )} -
- - {x.expires_at ? ( - dayjs(x.expires_at).isBefore(dayjs()) ? ( - + {filteredTokens?.map((x) => ( + + + + + +
+ + +
-
-
- ) - })} + + + { + setToken(x) + setIsOpen(true) + }} + > + +

Delete token

+
+
+ +
+ + + ))} setIsOpen(false)} onConfirm={() => { - if (token) onDeleteToken(token.id) + if (token) deleteToken({ id: token.id }) }} >

diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner.tsx new file mode 100644 index 00000000000..bb055d856e2 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner.tsx @@ -0,0 +1,67 @@ +import { X } from 'lucide-react' +import { toast } from 'sonner' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { TokenPermissionsSection } from './TokenPermissionSection' +import { useGroupedPermissions } from '../hooks/useGroupedPermissions' + +interface AccessTokenNewBannerProps { + token: T + onClose: () => void + getTokenValue: (token: T) => string + getTokenPermissions?: (token: T) => string[] | undefined + title?: string + description?: string +} + +export const AccessTokenNewBanner = ({ + token, + onClose, + getTokenValue, + getTokenPermissions, + title = 'Successfully generated a new token!', + description = 'Copy this access token and store it in a secure place. You will not be able to see it again.', +}: AccessTokenNewBannerProps) => { + const tokenPermissions = getTokenPermissions?.(token) + const { groupedPermissions, totalCount } = useGroupedPermissions(tokenPermissions) + + return ( + } + className="w-7 h-7 absolute top-2.5 right-2.5" + onClick={onClose} + /> + } + > +

+

{description}

+
+ {}} + onCopy={() => toast.success('Token copied to clipboard')} + /> +
+ + {tokenPermissions && tokenPermissions.length > 0 && ( + + )} +
+ + ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/PermissionList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/PermissionList.tsx new file mode 100644 index 00000000000..6353f874dcb --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/PermissionList.tsx @@ -0,0 +1,28 @@ +interface PermissionsListProps { + groupedPermissions: Record +} + +export const PermissionsList = ({ groupedPermissions }: PermissionsListProps) => { + return ( +
+ {Object.entries(groupedPermissions).map(([accessLevel, resources]) => ( +
+ + {accessLevel}: + +
+ {resources.map((resource, index) => ( + + {resource} + {index < resources.length - 1 ? ',' : '.'} + + ))} +
+
+ ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/TokenPermissionSection.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/TokenPermissionSection.tsx new file mode 100644 index 00000000000..c5b99c5d0c7 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/TokenPermissionSection.tsx @@ -0,0 +1,55 @@ +import { ChevronDown, ChevronRight } from 'lucide-react' +import { useState } from 'react' +import { Button } from 'ui' +import { + Collapsible_Shadcn_ as Collapsible, + CollapsibleContent_Shadcn_ as CollapsibleContent, + CollapsibleTrigger_Shadcn_ as CollapsibleTrigger, +} from 'ui' +import { PermissionsList } from './PermissionList' + +const PERMISSIONS_COLLAPSE_THRESHOLD = 5 + +interface TokenPermissionsSectionProps { + groupedPermissions: Record + totalCount: number +} + +export const TokenPermissionsSection = ({ + groupedPermissions, + totalCount, +}: TokenPermissionsSectionProps) => { + const [permissionsOpen, setPermissionsOpen] = useState(false) + const shouldCollapse = totalCount > PERMISSIONS_COLLAPSE_THRESHOLD + + if (totalCount === 0) return null + + return ( +
+ {shouldCollapse ? ( + + + + + + + + + ) : ( + <> +

Permissions assigned to this token:

+ + + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx new file mode 100644 index 00000000000..53f799da494 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from 'ui' +import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table' + +export const RowLoading = () => ( + + + + + + + + + + + + + + + + + +) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx new file mode 100644 index 00000000000..b36702b25f9 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx @@ -0,0 +1,44 @@ +import { Card, CardContent, cn } from 'ui' +import { + Table, + TableBody, + TableHead, + TableHeader, + TableHeadSort, + TableRow, +} from 'ui/src/components/shadcn/ui/table' +import { AccessTokenSort, AccessTokenSortColumn } from '../AccessToken.types' + +const tableHeaderClass = 'text-left font-mono uppercase text-xs text-foreground-lighter py-2' + +interface TableContainerProps { + children: React.ReactNode + sort: AccessTokenSort + onSortChange: (column: AccessTokenSortColumn) => void +} + +export const TableContainer = ({ children, sort, onSortChange }: TableContainerProps) => ( + + + + + + Token + + + Last used + + + + + Expires + + + + + + {children} +
+
+
+) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TokenCells.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TokenCells.tsx new file mode 100644 index 00000000000..b1cf4f95c1a --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TokenCells.tsx @@ -0,0 +1,62 @@ +import dayjs from 'dayjs' +import { TableCell } from 'ui/src/components/shadcn/ui/table' +import { TimestampInfo } from 'ui-patterns/TimestampInfo' + +interface TokenNameCellProps { + name: string + tokenAlias: string +} + +export const TokenNameCell = ({ name, tokenAlias }: TokenNameCellProps) => ( + +

+ {name} +

+

+ {tokenAlias} +

+
+) + +interface LastUsedCellProps { + lastUsedAt: string | null | undefined +} + +export const LastUsedCell = ({ lastUsedAt }: LastUsedCellProps) => ( + + {lastUsedAt ? ( + + ) : ( +

Never used

+ )} +
+) + +interface ExpiresCellProps { + expiresAt: string | null | undefined +} + +export const ExpiresCell = ({ expiresAt }: ExpiresCellProps) => ( + + {expiresAt ? ( + dayjs(expiresAt).isBefore(dayjs()) ? ( + + ) : ( + + ) + ) : ( +

Never

+ )} +
+) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokens.constants.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokens.constants.ts deleted file mode 100644 index 745c2f89ca7..00000000000 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokens.constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -import dayjs from 'dayjs' - -export const NON_EXPIRING_TOKEN_VALUE = 'never' -export const CUSTOM_EXPIRY_VALUE = 'custom' - -export const ExpiresAtOptions: Record = { - hour: { - value: dayjs().add(1, 'hour').toISOString(), - label: '1 hour', - }, - day: { - value: dayjs().add(1, 'days').toISOString(), - label: '1 day', - }, - week: { - value: dayjs().add(7, 'days').toISOString(), - label: '7 days', - }, - month: { - value: dayjs().add(30, 'days').toISOString(), - label: '30 days', - }, - never: { - value: NON_EXPIRING_TOKEN_VALUE, - label: 'Never', - }, - custom: { - value: CUSTOM_EXPIRY_VALUE, - label: 'Custom', - }, -} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenButton.tsx similarity index 84% rename from apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.tsx rename to apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenButton.tsx index a17eaa5cce8..c89aa724d12 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenButton.tsx @@ -8,13 +8,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from 'ui' -import { NewAccessTokenDialog } from './NewAccessTokenDialog' +import { NewTokenDialog } from './NewTokenDialog' +import { type NewAccessToken } from 'data/access-tokens/access-tokens-create-mutation' export interface NewAccessTokenButtonProps { - onCreateToken: (token: any) => void + onCreateToken: (token: NewAccessToken) => void } -export const NewAccessTokenButton = ({ onCreateToken }: NewAccessTokenButtonProps) => { +export const NewTokenButton = ({ onCreateToken }: NewAccessTokenButtonProps) => { const [visible, setVisible] = useState(false) const [tokenScope, setTokenScope] = useState<'V0' | undefined>(undefined) @@ -55,7 +56,7 @@ export const NewAccessTokenButton = ({ onCreateToken }: NewAccessTokenButtonProp
- void - onCreateToken: (token: any) => void + onCreateToken: (token: NewAccessToken) => void } -export const NewAccessTokenDialog = ({ +export const NewTokenDialog = ({ open, tokenScope, onOpenChange, @@ -64,18 +68,18 @@ export const NewAccessTokenDialog = ({ const form = useForm>({ resolver: zodResolver(TokenSchema), - defaultValues: { tokenName: '', expiresAt: ExpiresAtOptions['month'].value }, + defaultValues: { tokenName: '', expiresAt: EXPIRES_AT_OPTIONS['month'].value }, mode: 'onChange', }) const { mutate: createAccessToken, isPending } = useAccessTokenCreateMutation() const onSubmit: SubmitHandler> = async (values) => { - // Use custom date if custom option is selected - let expiresAt = values.expiresAt + let expiresAt: string | undefined if (isCustomExpiry && customExpiryDate) { - // Use the date from the TokensDatePicker expiresAt = customExpiryDate.date + } else { + expiresAt = getExpirationDate(values.expiresAt || '') } createAccessToken( @@ -201,11 +205,13 @@ export const NewAccessTokenDialog = ({ - {Object.values(ExpiresAtOptions).map((option) => ( - - {option.label} - - ))} + {Object.values(EXPIRES_AT_OPTIONS).map( + (option: { value: string; label: string }) => ( + + {option.label} + + ) + )} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.test.tsx b/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.test.tsx deleted file mode 100644 index 41a45511e54..00000000000 --- a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenButton.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { faker } from '@faker-js/faker' -import { screen, waitFor } from '@testing-library/dom' -import userEvent from '@testing-library/user-event' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -import { render } from 'tests/helpers' -import { addAPIMock } from 'tests/lib/msw' -import { NewAccessTokenButton } from './NewAccessTokenButton' - -describe(`NewAccessTokenButton`, () => { - beforeEach(() => { - addAPIMock({ - method: `post`, - path: `/platform/profile/access-tokens`, - response: { - name: faker.lorem.word(), - scope: faker.helpers.arrayElement(['V0', undefined]), - created_at: faker.date.past().toISOString(), - expires_at: null, - id: faker.number.int(), - token_alias: faker.lorem.words(), - token: faker.lorem.words(), - last_used_at: faker.date.recent().toISOString(), - }, - }) - }) - - it(`generates regular tokens`, async () => { - const onCreateToken = vi.fn() - render() - - const dialogTrigger = screen.getByRole(`button`, { name: `Generate new token` }) - await userEvent.click(dialogTrigger) - - // Fill in the token name - const nameInput = screen.getByLabelText(`Name`) - await userEvent.type(nameInput, `test`) - - // Verify the form is open and the name field works - expect(nameInput).toHaveValue(`test`) - expect(screen.getByRole(`button`, { name: `Generate token` })).toBeInTheDocument() - }) - - it(`generates experimental tokens`, async () => { - const onCreateToken = vi.fn() - render() - - const dropdownTrigger = screen.getByTitle(`Choose token scope`) - await userEvent.click(dropdownTrigger) - - const experimentalMenuItem = await screen.findByRole(`menuitem`, { - name: `Generate token for experimental API`, - }) - await userEvent.click(experimentalMenuItem) - - await waitFor(() => { - expect( - screen.getByRole(`heading`, { name: `Generate token for experimental API` }) - ).toBeInTheDocument() - expect( - screen.getByText( - `The experimental API provides additional endpoints which allows you to manage your organizations and projects.` - ) - ).toBeInTheDocument() - }) - - // Fill in the token name - const nameInput = screen.getByLabelText(`Name`) - await userEvent.type(nameInput, `test`) - - // Verify the form is open and the name field works - expect(nameInput).toHaveValue(`test`) - expect(screen.getByRole(`button`, { name: `Generate token` })).toBeInTheDocument() - }) - - it(`resets the form on close/cancel`, async () => { - render() - - // pass 1: open dialog and fill form - const dialogTrigger = screen.getByRole(`button`, { name: `Generate new token` }) - await userEvent.click(dialogTrigger) - - let nameInput = screen.getByLabelText(`Name`) - await userEvent.type(nameInput, `cancel button test`) - expect(nameInput).toHaveValue(`cancel button test`) - - // reset the form by pressing the cancel button - const cancelButton = screen.getByRole(`button`, { name: `Cancel` }) - await userEvent.click(cancelButton) - - // pass 2: check that the form is reset, then fill it again - await userEvent.click(dialogTrigger) - - nameInput = screen.getByLabelText(`Name`) - expect(nameInput).not.toHaveValue(`cancel button test`) - - await userEvent.type(nameInput, `close modal test`) - expect(nameInput).toHaveValue(`close modal test`) - - // reset the form by closing the dialog - await userEvent.keyboard(`{Escape}`) - - // pass 3: check that the form has been reset again - await userEvent.click(dialogTrigger) - - nameInput = screen.getByLabelText(`Name`) - expect(nameInput).not.toHaveValue(`close modal test`) - }) - - // it(`shows validation error when no permissions are configured`, async () => { - // const onCreateToken = vi.fn() - // render() - - // const dialogTrigger = screen.getByRole(`button`, { name: `Generate new token` }) - // await userEvent.click(dialogTrigger) - - // // Fill in the token name - // const nameInput = screen.getByLabelText(`Name`) - // await userEvent.type(nameInput, `test`) - - // // Try to submit without adding permissions - // const generateButton = screen.getByRole(`button`, { name: `Generate token` }) - // await userEvent.click(generateButton) - - // // The form should not submit and onCreateToken should not be called - // // because validation prevents submission when no permissions are configured - // expect(onCreateToken).not.toHaveBeenCalled() - - // // The form should still be open and the button should still be enabled - // expect(screen.getByRole(`button`, { name: `Generate token` })).toBeInTheDocument() - // }) -}) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/NewTokenBanner.tsx b/apps/studio/components/interfaces/Account/AccessTokens/NewTokenBanner.tsx deleted file mode 100644 index ccdb05bcd83..00000000000 --- a/apps/studio/components/interfaces/Account/AccessTokens/NewTokenBanner.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { X } from 'lucide-react' -import { toast } from 'sonner' - -import { NewAccessToken } from 'data/access-tokens/access-tokens-create-mutation' -import { Button } from 'ui' -import { Admonition } from 'ui-patterns' -import { Input } from 'ui-patterns/DataInputs/Input' - -interface NewTokenBannerProps { - token: NewAccessToken - onClose: () => void -} - -export const NewTokenBanner = ({ token, onClose }: NewTokenBannerProps) => { - return ( - -

- Do copy this access token and store it in a secure place - you will not be able to see - it again. -

-
- {}} - onCopy={() => toast.success('Token copied to clipboard')} - /> -
-
- } - > - + + + + + + No resources found. + + +
+ {ACCESS_TOKEN_RESOURCES.map((resource) => { + const isChecked = permissionRows.some( + (row: PermissionRow) => row.resource === resource.resource + ) + return ( + handleToggleResource(resource)} + className="text-foreground" + > +
+ handleToggleResource(resource)} + onClick={(e) => e.stopPropagation()} + /> + +
+ + {resource.title} + +
+
+
+ ) + })} +
+
+
+
+
+ + ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx new file mode 100644 index 00000000000..3a06202b1a9 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx @@ -0,0 +1,172 @@ +import { Path, PathValue } from 'react-hook-form' +import { ChevronDown } from 'lucide-react' +import { + Button, + Popover_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Checkbox_Shadcn_, + WarningIcon, +} from 'ui' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { X, RotateCcw } from 'lucide-react' +import { ACCESS_TOKEN_RESOURCES } from '../../../AccessToken.constants' +import { PermissionRow, PermissionsFormValues, PermissionsProps } from './Permissions.types' +import { sortActions } from './Permissions.utils' +import { formatAccessText } from '../../../AccessToken.utils' +import { PermissionResourceSelector } from './PermissionResourceSelector' + +export const Permissions = ({ + setValue, + watch, + resourceSearchOpen, + setResourceSearchOpen, +}: PermissionsProps) => { + const permissionRows = (watch('permissionRows' as Path) || []) as PermissionRow[] + + return ( +
+
+
+ Configure permissions +
+ {permissionRows.length > 0 && ( + { + setValue( + 'permissionRows' as Path, + [] as PathValue> + ) + }} + icon={} + tooltip={{ + content: { + side: 'top', + align: 'center', + alignOffset: -10, + text: 'Reset all permissions', + }, + }} + /> + )} + +
+
+ + {permissionRows.length === 0 ? ( +
+

No permissions configured yet.

+
+ ) : ( +
+ {permissionRows.map((row, index) => { + const selectedResource = ACCESS_TOKEN_RESOURCES.find( + (r) => r.resource === row.resource + ) + return ( +
+
+
+
+
+ + {selectedResource?.title} + +
+
+
+
+ {selectedResource && ( + + + + + +
+ {sortActions(selectedResource.actions).map((action) => ( + + ))} +
+
+
+ )} +
+
+ {index < permissionRows.length - 1 &&
} +
+ ) + })} +
+ )} +
+ +
+ + + Once you've set these permissions, you cannot edit them. + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts new file mode 100644 index 00000000000..ffb26fe487a --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts @@ -0,0 +1,33 @@ +import { FieldValues, UseFormSetValue, UseFormWatch } from 'react-hook-form' + +export interface PermissionResource { + resource: string + title: string + actions: string[] +} + +export interface PermissionRow { + resource: string + actions: string[] +} + +export interface PermissionsFormValues extends FieldValues { + permissionRows?: PermissionRow[] +} + +export interface PermissionsProps< + TFormValues extends PermissionsFormValues = PermissionsFormValues, +> { + setValue: UseFormSetValue + watch: UseFormWatch + resourceSearchOpen: boolean + setResourceSearchOpen: (open: boolean) => void +} + +export interface PermissionResourceSelectorProps { + open: boolean + onOpenChange: (open: boolean) => void + permissionRows: PermissionRow[] + setValue: UseFormSetValue + align?: 'center' | 'end' | 'start' +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.utils.ts b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.utils.ts new file mode 100644 index 00000000000..059d21d26d4 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.utils.ts @@ -0,0 +1,48 @@ +import { PermissionResource, PermissionRow } from './Permissions.types' + +const getBestAction = (actions: string[]): string => { + const availableActions = actions.filter((a) => a !== 'no access') + + if (availableActions.length === 0) return 'no access' + + const priority = ['read', 'write', 'create', 'delete', 'read-write'] + + for (const priorityAction of priority) { + if (availableActions.includes(priorityAction)) { + return priorityAction + } + } + + return availableActions[0] +} + +export const sortActions = (actions: string[]): string[] => { + const sorted: string[] = [] + const remaining = [...actions] + const priority = ['read', 'write', 'create', 'delete'] + + for (const action of priority) { + const index = remaining.indexOf(action) + if (index !== -1) { + sorted.push(action) + remaining.splice(index, 1) + } + } + + sorted.push(...remaining) + return sorted +} + +export const togglePermissionResource = ( + permissionRows: PermissionRow[], + resource: PermissionResource +): PermissionRow[] => { + const isAlreadyAdded = permissionRows.some((row) => row.resource === resource.resource) + + if (isAlreadyAdded) { + return permissionRows.filter((row) => row.resource !== resource.resource) + } + + const defaultActions = resource.actions.includes('read') ? ['read'] : [resource.actions[0]] + return [...permissionRows, { resource: resource.resource, actions: defaultActions }] +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.tsx new file mode 100644 index 00000000000..d7bc2fea0b8 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.tsx @@ -0,0 +1,115 @@ +import { useProfile } from 'lib/profile' +import { FormControl_Shadcn_, FormField_Shadcn_, cn } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { useOrgAndProjectData } from '../../../hooks/useOrgAndProjectData' +import { ResourceAccessProps } from './ResourceAccess.types' +import { ResourceMultiSelector } from './ResourceMultiSelector' +import { ResourceOption } from './ResourceOption' + +const RESOURCE_OPTIONS = [ + { + value: 'all-orgs' as const, + label: 'Everything', + description: 'Access to all projects across all organizations you have access to.', + }, + { + value: 'selected-orgs' as const, + label: 'Selected orgs', + description: 'Access only to the organizations you have specified.', + }, + { + value: 'selected-projects' as const, + label: 'Selected projects', + description: 'Access only to the projects you have specified.', + }, +] + +export const ResourceAccess = ({ control, resourceAccess, setValue }: ResourceAccessProps) => { + const { profile } = useProfile() + + const { organizations, projects, isLoadingOrgs, isLoadingProjects } = useOrgAndProjectData({ + enabled: !!profile, + }) + + const handleResourceAccessChange = (newValue: string, currentValue: string) => { + if (newValue !== currentValue) { + setValue('selectedOrganizations', []) + setValue('selectedProjects', []) + } + } + + return ( +
+ ( + + +
+
+ Resource access level + {RESOURCE_OPTIONS.map((option) => ( + { + handleResourceAccessChange(option.value, field.value) + field.onChange(option.value) + }} + /> + ))} +
+ +

+ {RESOURCE_OPTIONS.find((opt) => opt.value === field.value)?.description} +

+
+
+
+ )} + /> + + {resourceAccess === 'selected-orgs' && ( + ( + ({ id: org.slug, name: org.name }))} + isLoading={isLoadingOrgs} + fieldName="selectedOrganizations" + label="Select organizations" + loadingMessage="Loading organizations..." + emptyMessage="No organizations available" + /> + )} + /> + )} + + {resourceAccess === 'selected-projects' && ( + ( + ({ id: project.ref, name: project.name }))} + isLoading={isLoadingProjects} + fieldName="selectedProjects" + label="Select projects" + loadingMessage="Loading projects..." + emptyMessage="No projects available" + /> + )} + /> + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.types.ts b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.types.ts new file mode 100644 index 00000000000..df1409affc8 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceAccess.types.ts @@ -0,0 +1,30 @@ +import { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form' +import { TokenFormValues } from '../../../AccessToken.schemas' + +export interface ResourceAccessProps { + control: Control + resourceAccess: string + setValue: UseFormSetValue +} + +export interface ResourceItem { + id: string + name: string +} + +export interface ResourceMultiSelectorProps { + field: ControllerRenderProps + items: ResourceItem[] + isLoading: boolean + fieldName: string + label: string + loadingMessage: string + emptyMessage: string +} + +export interface ResourceOptionProps { + value: string + label: string + isSelected: boolean + onChange: () => void +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceMultiSelector.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceMultiSelector.tsx new file mode 100644 index 00000000000..b32619fd883 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceMultiSelector.tsx @@ -0,0 +1,68 @@ +import { useMemo } from 'react' +import { FormControl_Shadcn_ } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorItem, + MultiSelectorList, + MultiSelectorTrigger, +} from 'ui-patterns/multi-select' +import { ResourceMultiSelectorProps } from './ResourceAccess.types' + +export const ResourceMultiSelector = ({ + field, + items, + isLoading, + fieldName, + label, + loadingMessage, + emptyMessage, +}: ResourceMultiSelectorProps) => { + const idToName = useMemo(() => new Map(items.map((item) => [item.id, item.name])), [items]) + + const displayValues = (field.value || []).map((id: string) => idToName.get(id) || id) + + const handleValuesChange = (names: string[]) => { + const ids = names + .map((name) => { + for (const [id, itemName] of idToName.entries()) { + if (itemName === name) return id + } + return name + }) + .filter(Boolean) + field.onChange(ids) + } + + return ( + + + + + + {isLoading ? ( +
{loadingMessage}
+ ) : items.length === 0 ? ( +
{emptyMessage}
+ ) : ( + + {items.map((item) => ( + + {item.name} + + ))} + + )} +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceOption.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceOption.tsx new file mode 100644 index 00000000000..0aed24cf0b2 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceOption.tsx @@ -0,0 +1,42 @@ +import { cn } from 'ui' +import { Box } from 'lucide-react' +import { Check } from 'lucide-react' + +export const ResourceOption = ({ + value, + label, + isSelected, + onChange, +}: { + value: string + label: string + isSelected: boolean + onChange: () => void +}) => ( + +) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton.tsx new file mode 100644 index 00000000000..c95257038f9 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react' + +import { Button } from 'ui' +import { type NewScopedAccessToken } from 'data/scoped-access-tokens/scoped-access-token-create-mutation' +import { NewScopedTokenSheet } from './NewScopedTokenSheet' + +export interface NewScopedTokenButtonProps { + onCreateToken: (token: NewScopedAccessToken) => void +} + +export const NewScopedTokenButton = ({ onCreateToken }: NewScopedTokenButtonProps) => { + const [visible, setVisible] = useState(false) + + return ( + <> + + + + + ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx new file mode 100644 index 00000000000..2eaf5380d36 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx @@ -0,0 +1,320 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import dayjs from 'dayjs' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' +import { useCallback, useState } from 'react' +import { type SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' + +import { + useAccessTokenCreateMutation, + type NewScopedAccessToken, + type ScopedAccessTokenCreateVariables, +} from 'data/scoped-access-tokens/scoped-access-token-create-mutation' +import { + Button, + Form_Shadcn_, + ScrollArea, + Separator, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetDescription, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { BasicInfo } from './Form/BasicInfo' +import { Permissions } from './Form/Permissions/Permissions' +import { ResourceAccess } from './Form/ResourceAccess/ResourceAccess' +import { + CUSTOM_EXPIRY_VALUE, + EXPIRES_AT_OPTIONS, + type ScopedAccessTokenPermission, +} from '../AccessToken.constants' +import { useOrgAndProjectData } from '../hooks/useOrgAndProjectData' +import { mapPermissionToFGA, getExpirationDate } from '../AccessToken.utils' +import { TokenSchema, type TokenFormValues } from '../AccessToken.schemas' + +export interface NewScopedTokenSheetProps { + visible: boolean + onOpenChange: (open: boolean) => void + tokenScope: 'V0' | undefined + onCreateToken: (token: NewScopedAccessToken) => void +} + +export const NewScopedTokenSheet = ({ + visible, + onOpenChange, + tokenScope, + onCreateToken, +}: NewScopedTokenSheetProps) => { + const [resourceSearchOpen, setResourceSearchOpen] = useState(false) + const { organizations, projects } = useOrgAndProjectData() + + const form = useForm({ + resolver: zodResolver(TokenSchema), + defaultValues: { + tokenName: '', + expiresAt: EXPIRES_AT_OPTIONS['month'].value, + customExpiryDate: undefined, + resourceAccess: 'all-orgs', + selectedOrganizations: [], + selectedProjects: [], + permissionRows: [], + }, + mode: 'onChange', + }) + const { mutate: createAccessToken, isPending } = useAccessTokenCreateMutation() + + const resourceAccess = form.watch('resourceAccess') + const expiresAt = form.watch('expiresAt') + const permissionRows = form.watch('permissionRows') || [] + + const onSubmit: SubmitHandler = async (values) => { + if (!permissionRows || permissionRows.length === 0) { + toast.error('Please configure at least one permission.') + return + } + + const hasValidPermissions = permissionRows.every( + (row) => row.resource && row.actions && row.actions.length > 0 + ) + if (!hasValidPermissions) { + toast.error('Please ensure all permissions have both resource and action selected.') + return + } + + if (values.resourceAccess === 'selected-orgs') { + const selectedOrgs = values.selectedOrganizations || [] + + if (selectedOrgs.length === 0) { + toast.error('Please select at least one organization.') + return + } + + const availableOrgSlugs = organizations.map((org) => org.slug) + const invalidOrgs = selectedOrgs.filter((slug) => !availableOrgSlugs.includes(slug)) + + if (invalidOrgs.length > 0) { + toast.error( + `You don't have access to the following organization(s): ${invalidOrgs.join(', ')}` + ) + return + } + } + + if (values.resourceAccess === 'selected-projects') { + const selectedProjects = values.selectedProjects || [] + + if (selectedProjects.length === 0) { + toast.error('Please select at least one project.') + return + } + + const availableProjectRefs = projects.map((project) => project.ref) + const invalidProjects = selectedProjects.filter((ref) => !availableProjectRefs.includes(ref)) + + if (invalidProjects.length > 0) { + toast.error( + `You don't have access to the following project(s): ${invalidProjects.join(', ')}` + ) + return + } + } + + const finalExpiresAt = + values.expiresAt === CUSTOM_EXPIRY_VALUE + ? values.customExpiryDate + : getExpirationDate(values.expiresAt || '') + + const permissions = permissionRows + .flatMap((row) => { + const { resource, actions } = row + return actions.flatMap((action) => mapPermissionToFGA(resource, action)) + }) + .filter(Boolean) as ScopedAccessTokenPermission[] + + if (!permissions || permissions.length === 0) { + toast.error('Please configure at least one valid permission.') + return + } + + const finalPayload: ScopedAccessTokenCreateVariables = { + name: values.tokenName, + permissions, + } + + if (finalExpiresAt) { + finalPayload.expires_at = finalExpiresAt + } + + if ( + values.resourceAccess === 'selected-orgs' && + values.selectedOrganizations && + values.selectedOrganizations.length > 0 + ) { + finalPayload.organization_slugs = values.selectedOrganizations + } else if ( + values.resourceAccess === 'selected-projects' && + values.selectedProjects && + values.selectedProjects.length > 0 + ) { + finalPayload.project_refs = values.selectedProjects + } + + if (!finalPayload.name || finalPayload.name.trim() === '') { + toast.error('Please enter a token name.') + return + } + + if (!finalPayload.permissions || finalPayload.permissions.length === 0) { + toast.error('Please configure at least one permission.') + return + } + + createAccessToken(finalPayload, { + onSuccess: (data) => { + toast.success('Access token created successfully') + onCreateToken(data) + handleClose() + }, + onError: (error) => { + if (error.message && error.message.includes("don't have access")) { + toast.error( + `Access Error: ${error.message}. Please verify you have access to the selected resources.` + ) + } else { + toast.error(`Failed to create access token: ${error.message}`) + } + }, + }) + } + + const handleClose = () => { + form.reset({ + tokenName: '', + expiresAt: EXPIRES_AT_OPTIONS['month'].value, + customExpiryDate: undefined, + resourceAccess: 'all-orgs', + selectedOrganizations: [], + selectedProjects: [], + permissionRows: [], + }) + onOpenChange(false) + } + + const handleCustomDateChange = useCallback( + (date: { date: string } | undefined) => { + form.setValue('customExpiryDate', date?.date, { shouldValidate: true }) + }, + [form] + ) + + const handleCustomExpiryChange = useCallback( + (isCustom: boolean) => { + if (isCustom && !form.getValues('customExpiryDate')) { + form.setValue('customExpiryDate', dayjs().endOf('day').toISOString(), { + shouldValidate: true, + }) + } + if (!isCustom) { + form.setValue('customExpiryDate', undefined, { shouldValidate: true }) + } + }, + [form] + ) + + return ( + { + if (!open) { + handleClose() + } else { + onOpenChange(open) + } + }} + > + + + + {tokenScope === 'V0' ? 'Generate token for experimental API' : 'Generate New Token'} + + + A form to generate a new scoped access token. + + + +
+ {tokenScope === 'V0' && ( +
+ +

+ These include deleting organizations and projects which cannot be undone. As + such, be very careful when using this API. +

+
+ +
+ + } + /> +
+ )} + + +
+ + + + + +
+
+
+
+ +
+ + +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList.tsx new file mode 100644 index 00000000000..85307d63561 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList.tsx @@ -0,0 +1,187 @@ +import AlertError from 'components/ui/AlertError' +import { useScopedAccessTokenDeleteMutation } from 'data/scoped-access-tokens/scoped-access-tokens-delete-mutation' +import { + ScopedAccessToken, + useScopedAccessTokensQuery, +} from 'data/scoped-access-tokens/scoped-access-token-query' +import { MoreVertical, Trash, Key } from 'lucide-react' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table' +import { parseAsStringLiteral, useQueryState } from 'nuqs' + +import { + ACCESS_TOKEN_SORT_VALUES, + AccessTokenSort, + AccessTokenSortColumn, +} from '../AccessToken.types' +import { handleSortChange, filterAndSortTokens } from '../AccessToken.utils' +import { TableContainer } from '../AccessTokenTable/TableContainer' +import { RowLoading } from '../AccessTokenTable/RowLoading' +import { TokenNameCell, LastUsedCell, ExpiresCell } from '../AccessTokenTable/TokenCells' +import { ViewTokenSheet } from './ViewTokenSheet' + +export interface ScopedTokenListProps { + searchString?: string + onDeleteSuccess: (id: string | number) => void +} + +export const ScopedTokenList = ({ searchString = '', onDeleteSuccess }: ScopedTokenListProps) => { + const [isOpen, setIsOpen] = useState(false) + const [token, setToken] = useState(undefined) + const [viewToken, setViewToken] = useState(undefined) + const [isViewSheetOpen, setIsViewSheetOpen] = useState(false) + const [sort, setSort] = useQueryState( + 'sort', + parseAsStringLiteral(ACCESS_TOKEN_SORT_VALUES).withDefault('created_at:desc') + ) + + const { data: tokensData, error, isPending: isLoading, isError } = useScopedAccessTokensQuery() + + const tokens = tokensData?.tokens + + const { mutate: deleteToken } = useScopedAccessTokenDeleteMutation({ + onSuccess: (_, vars) => { + onDeleteSuccess(vars.id) + toast.success('Successfully deleted access token') + setIsOpen(false) + }, + onError: (error) => { + toast.error(`Failed to delete access token: ${error.message}`) + }, + }) + + const onSortChange = (column: AccessTokenSortColumn) => { + handleSortChange(sort, column, setSort) + } + + const filteredTokens = useMemo( + () => filterAndSortTokens(tokens, searchString, sort), + [tokens, searchString, sort] + ) + + const empty = filteredTokens?.length === 0 && !isLoading + + if (isError) { + return ( + + + + + + + + ) + } + + if (isLoading) { + return ( + + + + + ) + } + + if (empty) { + return ( + + + +

No scoped access tokens found

+

+ You do not have any scoped tokens created yet +

+
+
+
+ ) + } + + return ( + <> + + {filteredTokens?.map((x) => ( + + + + + +
+ + +
+
+
+ ))} +
+ + setIsOpen(false)} + onConfirm={() => { + if (token) deleteToken({ id: token.id as string }) + }} + > +

+ This action cannot be undone. Are you sure you want to delete "{token?.name}" token? +

+
+ + { + setIsViewSheetOpen(false) + setViewToken(undefined) + }} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx new file mode 100644 index 00000000000..f1810cf962c --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx @@ -0,0 +1,304 @@ +import dayjs from 'dayjs' +import { + Sheet, + SheetContent, + SheetHeader, + ScrollArea, + cn, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from 'ui' +import { useScopedAccessTokenQuery } from 'data/scoped-access-tokens/scoped-access-token-query' +import { DocsButton } from 'components/ui/DocsButton' +import { Card, CardContent } from 'ui' +import { ACCESS_TOKEN_RESOURCES } from '../AccessToken.constants' +import { useMemo } from 'react' +import { formatAccessText, getRealAccess } from '../AccessToken.utils' +import { useOrgAndProjectData } from '../hooks/useOrgAndProjectData' +import { TimestampInfo } from 'ui-patterns/TimestampInfo' + +interface ViewTokenSheetProps { + visible: boolean + tokenId: string | undefined + onClose: () => void +} + +export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProps) { + const { organizations, projects } = useOrgAndProjectData() + + const { + data: token, + isLoading: isTokenLoading, + error: tokenError, + } = useScopedAccessTokenQuery( + { id: tokenId! }, + { + enabled: visible && !!tokenId, + retry: 1, + retryDelay: 1000, + } + ) + + const groupedResourcesByAccess = useMemo(() => { + const grouped: Record = {} + + if (!token?.permissions) { + return grouped + } + + ACCESS_TOKEN_RESOURCES.forEach((resource) => { + const access = getRealAccess(resource.resource, token.permissions) + if (access !== 'no access') { + const formattedAccess = formatAccessText(access) + if (!grouped[formattedAccess]) { + grouped[formattedAccess] = [] + } + grouped[formattedAccess].push(resource.title) + } + }) + + return grouped + }, [token?.permissions]) + + const getResourceAccessInfo = () => { + const resources: Array<{ name: string; type: string; identifier: string }> = [] + + const organizationSlugs = token?.organization_slugs + if (organizationSlugs && Array.isArray(organizationSlugs) && organizationSlugs.length > 0) { + organizationSlugs.forEach((orgSlug: string) => { + const org = organizations.find((o) => o.slug === orgSlug) + resources.push({ + name: org?.name || orgSlug, + type: 'Organization', + identifier: orgSlug, + }) + }) + } + + const projectRefs = token?.project_refs + if (projectRefs && Array.isArray(projectRefs) && projectRefs.length > 0) { + projectRefs.forEach((projectRef: string) => { + const project = projects.find((p) => p.ref === projectRef) + resources.push({ + name: project?.name || projectRef, + type: 'Project', + identifier: projectRef, + }) + }) + } + + return resources + } + + return ( + <> + onClose()}> + + +

+ View access for {token?.name} +

+ +
+ +
+ {isTokenLoading && ( +
+

Loading token information...

+
+ )} + + {tokenError && ( +
+

+ Error loading token information. Please try again. +

+
+ )} + + {token && ( + <> +
+

Token Information

+ + + + + + + Info + + + Date + + + + + + +

Created

+
+ + {token?.created_at ? ( + + ) : ( + Unknown + )} + +
+ + +

Last used

+
+ + {token?.last_used_at ? ( + + ) : ( + Never + )} + +
+ + +

Expires

+
+ + {token?.expires_at ? ( + + ) : ( + Never + )} + +
+
+
+
+
+
+ +
+

Resource Access

+ + + + + + + Resource + + + Type + + + + + {getResourceAccessInfo().length > 0 ? ( + getResourceAccessInfo().map((resource, index) => ( + + +

{resource.name}

+
+ + {resource.type} + +
+ )) + ) : ( + + +

+ {(token?.organization_slugs && + token.organization_slugs.length > 0) || + (token?.project_refs && token.project_refs.length > 0) + ? 'This token has access to specific organizations and projects.' + : 'This token has access to all resources.'} +

+
+
+ )} +
+
+
+
+
+ +
+

Permissions

+ + + + + + + Permission + + + Access + + + + + {Object.keys(groupedResourcesByAccess).length === 0 ? ( + + +

+ No permissions configured for this token. +

+
+
+ ) : ( + Object.entries(groupedResourcesByAccess).map( + ([accessLevel, resources]) => { + return resources.map((resource) => ( + + +

+ {resource} +

+
+ + + {formatAccessText(accessLevel)} + + +
+ )) + } + ) + )} +
+
+
+
+
+ + )} +
+
+
+
+ + ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/hooks/useGroupedPermissions.ts b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useGroupedPermissions.ts new file mode 100644 index 00000000000..d69f3e23e1d --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useGroupedPermissions.ts @@ -0,0 +1,33 @@ +import { useMemo } from 'react' +import { ACCESS_TOKEN_RESOURCES } from '../AccessToken.constants' +import { getRealAccess, formatAccessText } from '../AccessToken.utils' + +export const useGroupedPermissions = (tokenPermissions: string[] | undefined) => { + const groupedPermissions = useMemo(() => { + const grouped: Record = {} + + if (!tokenPermissions || tokenPermissions.length === 0) { + return grouped + } + + ACCESS_TOKEN_RESOURCES.forEach((resource) => { + const access = getRealAccess(resource.resource, tokenPermissions) + if (access !== 'no access') { + const formattedAccess = formatAccessText(access) + if (!grouped[formattedAccess]) { + grouped[formattedAccess] = [] + } + grouped[formattedAccess].push(resource.title) + } + }) + + return grouped + }, [tokenPermissions]) + + const totalCount = Object.values(groupedPermissions).reduce( + (sum, resources) => sum + resources.length, + 0 + ) + + return { groupedPermissions, totalCount } +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData.ts b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData.ts new file mode 100644 index 00000000000..2ae0c8bc0b4 --- /dev/null +++ b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { useProjectsInfiniteQuery } from 'data/projects/projects-infinite-query' + +interface UseOrgAndProjectDataOptions { + enabled?: boolean +} + +export const useOrgAndProjectData = (options: UseOrgAndProjectDataOptions = {}) => { + const { enabled = true } = options + + const { data: organizations = [], isLoading: isLoadingOrgs } = useOrganizationsQuery({ enabled }) + + const { data: projectsData, isLoading: isLoadingProjects } = useProjectsInfiniteQuery({ + limit: 100, + }) + + const projects = useMemo( + () => projectsData?.pages.flatMap((page) => page.projects) ?? [], + [projectsData] + ) + + return { + organizations, + projects, + isLoadingOrgs, + isLoadingProjects, + } +} diff --git a/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx b/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx new file mode 100644 index 00000000000..43840b62216 --- /dev/null +++ b/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx @@ -0,0 +1,72 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { PropsWithChildren } from 'react' + +import { useFlag } from 'common' +import { NavMenu, NavMenuItem } from 'ui' +import { PageContainer } from 'ui-patterns/PageContainer' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderNavigationTabs, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' + +const AccessTokensLayout = ({ children }: PropsWithChildren) => { + const router = useRouter() + const scopedTokensEnabled = useFlag('scopedPAT') + + const navigationItems = [ + { + label: 'Classic Tokens', + href: `/account/tokens`, + id: 'classic-tokens', + }, + ...(scopedTokensEnabled + ? [ + { + label: 'Scoped Tokens', + href: `/account/tokens/scoped`, + id: 'scoped-tokens', + }, + ] + : []), + ] + + const title = 'Access Tokens' + const description = 'Create and manage access tokens for API authentication.' + + return ( + <> + + + + {title} + {description} + + + {navigationItems.length > 0 && ( + + + {navigationItems.map((item) => { + const isActive = router.asPath.split('?')[0] === item.href + return ( + + {item.label} + + ) + })} + + + )} + + + {children} + + + ) +} + +export default AccessTokensLayout diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx index 33b83ecf4fa..84640554271 100644 --- a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -71,7 +71,8 @@ const AccountLayout = ({ children, title }: PropsWithChildren { - const pathname = usePathname() - const links = [ - { - isActive: pathname === `/account/me`, - label: 'Preferences', - href: `/account/me`, - key: `/account/me`, - }, - { - isActive: pathname === `/account/tokens`, - label: 'Access Tokens', - href: `/account/tokens`, - key: `/account/tokens`, - }, - - { - isActive: pathname === `/account/security`, - label: 'Security', - href: `/account/security`, - key: `/account/security`, - }, - ] - - return ( - - -
{children}
-
- ) -} diff --git a/apps/studio/data/access-tokens/keys.ts b/apps/studio/data/access-tokens/keys.ts index 8d5e3a8f3da..e000d4a01bc 100644 --- a/apps/studio/data/access-tokens/keys.ts +++ b/apps/studio/data/access-tokens/keys.ts @@ -1,3 +1,4 @@ export const accessTokenKeys = { list: () => ['access-tokens'] as const, + detail: (id: string) => ['access-tokens', id] as const, } diff --git a/apps/studio/data/scoped-access-tokens/keys.ts b/apps/studio/data/scoped-access-tokens/keys.ts new file mode 100644 index 00000000000..f3dc146c2bf --- /dev/null +++ b/apps/studio/data/scoped-access-tokens/keys.ts @@ -0,0 +1,4 @@ +export const scopedAccessTokenKeys = { + list: () => ['scoped-access-tokens'] as const, + detail: (id: string) => ['scoped-access-tokens', id] as const, +} diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts b/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts new file mode 100644 index 00000000000..3c02d2a794a --- /dev/null +++ b/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts @@ -0,0 +1,55 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { components } from 'api-types' +import { handleError, post } from 'data/fetchers' +import type { ResponseError, UseCustomMutationOptions } from 'types' +import { scopedAccessTokenKeys } from './keys' + +export type ScopedAccessTokenCreateVariables = components['schemas']['CreateScopedAccessTokenBody'] + +export async function createScopedAccessToken({ + name, + expires_at, + organization_slugs, + permissions, + project_refs, +}: ScopedAccessTokenCreateVariables) { + const { data, error } = await post('/platform/profile/scoped-access-tokens', { + body: { name, organization_slugs, expires_at, permissions, project_refs }, + }) + + if (error) handleError(error) + + return data +} + +export type NewScopedAccessToken = components['schemas']['CreateScopedAccessTokenResponse'] + +export const useAccessTokenCreateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseCustomMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (vars) => createScopedAccessToken(vars), + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.list() }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create access token: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts b/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts new file mode 100644 index 00000000000..849b3812e93 --- /dev/null +++ b/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts @@ -0,0 +1,60 @@ +import { useQuery } from '@tanstack/react-query' + +import { components } from 'api-types' +import { get, handleError } from 'data/fetchers' +import type { ResponseError, UseCustomQueryOptions } from 'types' +import { scopedAccessTokenKeys } from './keys' + +export async function getScopedAccessTokens(signal?: AbortSignal) { + const { data, error } = await get('/platform/profile/scoped-access-tokens', { signal }) + + if (error) handleError(error) + + return data +} + +export async function getScopedAccessToken({ id }: { id: string }, signal?: AbortSignal) { + if (!id) throw new Error('Token ID is required') + + const { data, error } = await get('/platform/profile/scoped-access-tokens/{id}', { + params: { path: { id } }, + signal, + }) + + if (error) handleError(error) + + return data +} + +export type ScopedAccessTokensData = Awaited> +export type ScopedAccessTokensError = ResponseError + +export type ScopedAccessToken = + components['schemas']['GetScopedAccessTokensResponse']['tokens'][number] + +export type ScopedAccessTokenData = Awaited> + +export const useScopedAccessTokensQuery = ({ + enabled = true, + ...options +}: UseCustomQueryOptions = {}) => + useQuery({ + queryKey: scopedAccessTokenKeys.list(), + queryFn: ({ signal }) => getScopedAccessTokens(signal), + enabled, + ...options, + }) + +export const useScopedAccessTokenQuery = ( + { id }: { id: string }, + { + enabled = true, + ...options + }: UseCustomQueryOptions = {} +) => + useQuery({ + queryKey: scopedAccessTokenKeys.detail(id), + queryFn: ({ signal }) => getScopedAccessToken({ id }, signal), + enabled: enabled && typeof id !== 'undefined', + ...options, + }) diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts b/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts new file mode 100644 index 00000000000..701991ad40c --- /dev/null +++ b/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts @@ -0,0 +1,50 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { del, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { scopedAccessTokenKeys } from './keys' + +export type ScopedAccessTokenDeleteVariables = { + id: string +} + +export async function deleteScopedAccessToken({ id }: ScopedAccessTokenDeleteVariables) { + const { data, error } = await del('/platform/profile/scoped-access-tokens/{id}', { + params: { path: { id } }, + }) + + if (error) handleError(error) + return data +} + +type ScopedAccessTokenDeleteData = Awaited> + +export const useScopedAccessTokenDeleteMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (vars) => deleteScopedAccessToken(vars), + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.list() }) + await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.detail(variables.id) }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to delete access token: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/package.json b/apps/studio/package.json index 49615234eec..beec72b34b0 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -63,7 +63,7 @@ "@supabase/mcp-utils": "^0.3.2", "@supabase/pg-meta": "workspace:*", "@supabase/realtime-js": "catalog:", - "@supabase/shared-types": "0.1.83", + "@supabase/shared-types": "0.1.84", "@supabase/sql-to-rest": "^0.1.6", "@supabase/supabase-js": "catalog:", "@tanstack/react-query": "^5.0.0", diff --git a/apps/studio/pages/account/tokens.tsx b/apps/studio/pages/account/tokens.tsx index 33ed3334fab..f875a7b55a4 100644 --- a/apps/studio/pages/account/tokens.tsx +++ b/apps/studio/pages/account/tokens.tsx @@ -1,9 +1,9 @@ import { ExternalLink, Search } from 'lucide-react' import { useState } from 'react' - import { AccessTokenList } from 'components/interfaces/Account/AccessTokens/AccessTokenList' -import { NewAccessTokenButton } from 'components/interfaces/Account/AccessTokens/NewAccessTokenButton' -import { NewTokenBanner } from 'components/interfaces/Account/AccessTokens/NewTokenBanner' +import { NewTokenButton } from 'components/interfaces/Account/AccessTokens/Classic/NewTokenButton' +import { AccessTokenNewBanner } from '@/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner' +import AccessTokensLayout from 'components/layouts/AccessTokens/AccessTokensLayout' import AccountLayout from 'components/layouts/AccountLayout/AccountLayout' import AppLayout from 'components/layouts/AppLayout/AppLayout' import DefaultLayout from 'components/layouts/DefaultLayout' @@ -13,68 +13,54 @@ import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Button } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' -import { PageContainer } from 'ui-patterns/PageContainer' -import { - PageHeader, - PageHeaderDescription, - PageHeaderMeta, - PageHeaderSummary, - PageHeaderTitle, -} from 'ui-patterns/PageHeader' const UserAccessTokens: NextPageWithLayout = () => { const [newToken, setNewToken] = useState() const [searchString, setSearchString] = useState('') return ( - <> - - - - Access Tokens - - Create and manage personal access tokens for API authentication. - - - - - -
- {newToken && setNewToken(undefined)} />} -
- } - value={searchString} - onChange={(e: any) => setSearchString(e.target.value)} - name="search" - id="search" - placeholder="Filter tokens" - /> -
- - - -
-
- { - if (id === newToken?.id) setNewToken(undefined) - }} + +
+ {newToken && ( + setNewToken(undefined)} + getTokenValue={(token) => token.token} /> + )} +
+ } + value={searchString} + onChange={(e: React.ChangeEvent) => setSearchString(e.target.value)} + name="search" + id="search" + placeholder="Filter tokens" + /> +
+ + + +
- - + { + if (id === newToken?.id) setNewToken(undefined) + }} + /> +
+
) } diff --git a/apps/studio/pages/account/tokens/scoped.tsx b/apps/studio/pages/account/tokens/scoped.tsx new file mode 100644 index 00000000000..a2a259fa9a1 --- /dev/null +++ b/apps/studio/pages/account/tokens/scoped.tsx @@ -0,0 +1,88 @@ +import { ExternalLink, Search } from 'lucide-react' +import { useState } from 'react' + +import AccessTokensLayout from 'components/layouts/AccessTokens/AccessTokensLayout' +import { NewScopedTokenButton } from 'components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton' +import { AccessTokenNewBanner } from '@/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner' + +import { ScopedTokenList } from 'components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList' +import AccountLayout from 'components/layouts/AccountLayout/AccountLayout' +import AppLayout from 'components/layouts/AppLayout/AppLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' +import OrganizationLayout from 'components/layouts/OrganizationLayout' +import { NewScopedAccessToken } from 'data/scoped-access-tokens/scoped-access-token-create-mutation' +import type { NextPageWithLayout } from 'types' +import { Button } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' + +const ScopedTokens: NextPageWithLayout = () => { + const [searchString, setSearchString] = useState('') + const [newToken, setNewToken] = useState() + + return ( + +
+ {newToken && ( + setNewToken(undefined)} + getTokenValue={(token) => token.token} + getTokenPermissions={(token) => token.permissions} + /> + )} +
+ } + value={searchString} + onChange={(e: any) => setSearchString(e.target.value)} + name="search" + id="search" + placeholder="Filter by name" + /> +
+ + + +
+
+ + { + if (id === newToken?.id) setNewToken(undefined) + }} + /> +
+
+ ) +} + +ScopedTokens.getLayout = (page) => ( + + + + {page} + + + +) + +export default ScopedTokens diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32a96ed16e3..50881543cc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1173,8 +1173,8 @@ importers: specifier: 'catalog:' version: 2.94.1 '@supabase/shared-types': - specifier: 0.1.83 - version: 0.1.83 + specifier: 0.1.84 + version: 0.1.84 '@supabase/sql-to-rest': specifier: ^0.1.6 version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) @@ -9285,8 +9285,8 @@ packages: resolution: {integrity: sha512-Wq8olpCAGmN4y2DH2kUdlcakdzNHRCde72BFS8zK5ub46bBeSUoE9DqrfeNFWKaF2gCE/cmK8aTUTorZD9jdtQ==} engines: {node: '>=20.0.0'} - '@supabase/shared-types@0.1.83': - resolution: {integrity: sha512-hKXExQKLU9FvUCpDsMKtP/EN+qma5SL+pxd44D06OmwV1UnOUqBwgUvO0nUyF2vV32jKRNaWsf2o+IDJ3pATJA==} + '@supabase/shared-types@0.1.84': + resolution: {integrity: sha512-0bcV8Il0r5zybdY6gIWEmbJ/5VGGw60p9SOZ8tUe3x7iKxW/2H+CmPezyqQHuwYURxF4Gf18zJfENlaDyn+g8w==} '@supabase/sql-to-rest@0.1.6': resolution: {integrity: sha512-06KgjeINtc6405XQvfnchBE1azEsU8G2NElfadmvVHKmHa5l2bFzjbtFbpaYgpgTzccHlcDmBaCgedVf2Gyl8Q==} @@ -28445,7 +28445,7 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/shared-types@0.1.83': {} + '@supabase/shared-types@0.1.84': {} '@supabase/sql-to-rest@0.1.6(encoding@0.1.13)(supports-color@8.1.1)': dependencies: From 6b8c225245511c5016a46ddeb7db30726e7b8cf1 Mon Sep 17 00:00:00 2001 From: Ana <30495040+ana1337x@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:42:25 -0500 Subject: [PATCH 20/22] Add blog post: Supabase is now an official Claude connector (#42401) --- ...se-is-now-an-official-claude-connector.mdx | 55 ++++++++++++++++++ .../og.png | Bin 0 -> 52373 bytes .../screenshot-2.png | Bin 0 -> 113872 bytes .../thumb.png | Bin 0 -> 52373 bytes 4 files changed, 55 insertions(+) create mode 100644 apps/www/_blog/2026-02-03-supabase-is-now-an-official-claude-connector.mdx create mode 100644 apps/www/public/images/blog/2026-02-03-supabase-is-now-an-official-claude-connector/og.png create mode 100644 apps/www/public/images/blog/2026-02-03-supabase-is-now-an-official-claude-connector/screenshot-2.png create mode 100644 apps/www/public/images/blog/2026-02-03-supabase-is-now-an-official-claude-connector/thumb.png diff --git a/apps/www/_blog/2026-02-03-supabase-is-now-an-official-claude-connector.mdx b/apps/www/_blog/2026-02-03-supabase-is-now-an-official-claude-connector.mdx new file mode 100644 index 00000000000..ba4e7a09588 --- /dev/null +++ b/apps/www/_blog/2026-02-03-supabase-is-now-an-official-claude-connector.mdx @@ -0,0 +1,55 @@ +--- +title: 'Supabase is now an official Claude connector' +description: 'Connect your Supabase projects to Claude and manage your entire database infrastructure by telling Claude what you need.' +author: pedro_rodrigues +date: '2026-02-03' +categories: + - product +tags: + - ai + - claude + - mcp + - integrations +imgSocial: '2026-02-03-supabase-is-now-an-official-claude-connector/og.png' +imgThumb: '2026-02-03-supabase-is-now-an-official-claude-connector/thumb.png' +toc_depth: 2 +--- + +The Supabase MCP server is now listed on Claude's official connectors. Connect your Supabase projects to Claude and manage your entire database infrastructure by telling Claude what you need. + +With the Supabase MCP server, you can execute SQL queries, modify schemas, deploy edge functions, manage branches, and troubleshoot your projects without leaving your conversation with Claude. + +Ask Claude to check security advisors on your project and fix any issues. Request a schema change and Claude executes it. Deploy an edge function with a single prompt. + +
+