diff --git a/.cursor/rules/docs-graphql.mdc b/.cursor/rules/docs-graphql.mdc deleted file mode 100644 index d8bcbc5d932bc..0000000000000 --- a/.cursor/rules/docs-graphql.mdc +++ /dev/null @@ -1,267 +0,0 @@ ---- -description: Docs GraphQL Architecture -globs: apps/docs/resources/**/*.ts -alwaysApply: false ---- - -# Docs GraphQL Architecture - -## Overview - -The `/apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure. - -## Architecture Pattern - -Each GraphQL query follows this structure: - -``` -resources/ -├── queryObject/ -│ ├── queryObjectModel.ts # Data models and business logic -│ ├── queryObjectSchema.ts # GraphQL type definitions -│ ├── queryObjectResolver.ts # Query resolver and arguments -│ ├── queryObjectTypes.ts # TypeScript interfaces (optional) -│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional) -├── utils/ -│ ├── connections.ts # GraphQL connection/pagination utilities -│ └── fields.ts # GraphQL field selection utilities -├── rootSchema.ts # Main GraphQL schema with all queries -└── rootSync.ts # Root sync script for syncing to database -``` - -## Example queries - -1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content -2. **error** (`error/`) - Error code lookup for Supabase services -3. **schema** - GraphQL schema introspection - -## Key Files - -### `rootSchema.ts` -- Main GraphQL schema definition -- Imports all resolvers and combines them into the root query -- Defines the `RootQueryType` with all top-level fields - -### `utils/connections.ts` -- Provides `createCollectionType()` for paginated collections -- `GraphQLCollectionBuilder` for building collection responses -- Standard pagination arguments and edge/node patterns - -### `utils/fields.ts` -- `graphQLFields()` utility to analyze requested fields in resolvers -- Used for optimizing data fetching based on what fields are actually requested - -## Creating a New Top-Level Query - -To add a new GraphQL query, follow these steps: - -### 1. Create Query Folder Structure -```bash -mkdir resources/newQuery -touch resources/newQuery/newQueryModel.ts -touch resources/newQuery/newQuerySchema.ts -touch resources/newQuery/newQueryResolver.ts -``` - -### 2. Define GraphQL Schema (`newQuerySchema.ts`) -```typescript -import { GraphQLObjectType, GraphQLString } from 'graphql' - -export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const - -export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({ - name: 'NewQuery', - description: 'Description of what this query returns', - fields: { - id: { - type: GraphQLString, - description: 'Unique identifier', - }, - // Add other fields... - }, -}) -``` - -### 3. Create Data Model (`newQueryModel.ts`) - -> [!NOTE] -> The data model should be agnostic to GraphQL. It may import argument types -> from `~/__generated__/graphql`, but otherwise all functions and classes -> should be unaware of whether they are called for GraphQL resolution. - -> [!TIP] -> The types in `~/__generated__/graphql` for a new endpoint will not exist -> until the code generation is run in the next step. - -```typescript -import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql' -import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils' -import { Result } from '~/features/helpers.fn' -import { supabase } from '~/lib/supabase' - -export class NewQueryModel { - constructor(public readonly data: { - id: string - // other properties... - }) {} - - static async loadData( - args: RootQueryTypeNewQueryArgs, - requestedFields: Array - ): Promise> { - // Implement data fetching logic - const result = new Result( - await supabase() - .from('your_table') - .select('*') - // Add filters based on args - ) - .map((data) => data.map((item) => new NewQueryModel(item))) - .mapError(convertPostgrestToApiError) - - return result - } -} -``` - -### 4. Create Resolver (`newQueryResolver.ts`) -```typescript -import { GraphQLError, GraphQLNonNull, GraphQLString, type GraphQLResolveInfo } from 'graphql' -import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql' -import { convertUnknownToApiError } from '~/app/api/utils' -import { Result } from '~/features/helpers.fn' -import { graphQLFields } from '../utils/fields' -import { NewQueryModel } from './newQueryModel' -import { GRAPHQL_FIELD_NEW_QUERY, GraphQLObjectTypeNewQuery } from './newQuerySchema' - -async function resolveNewQuery( - _parent: unknown, - args: RootQueryTypeNewQueryArgs, - _context: unknown, - info: GraphQLResolveInfo -): Promise { - return ( - await Result.tryCatchFlat( - resolveNewQueryImpl, - convertUnknownToApiError, - args, - info - ) - ).match( - (data) => data, - (error) => { - console.error(`Error resolving ${GRAPHQL_FIELD_NEW_QUERY}:`, error) - return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message) - } - ) -} - -async function resolveNewQueryImpl( - args: RootQueryTypeNewQueryArgs, - info: GraphQLResolveInfo -): Promise> { - const fieldsInfo = graphQLFields(info) - const requestedFields = Object.keys(fieldsInfo) - return await NewQueryModel.loadData(args, requestedFields) -} - -export const newQueryRoot = { - [GRAPHQL_FIELD_NEW_QUERY]: { - description: 'Description of what this query does', - args: { - id: { - type: new GraphQLNonNull(GraphQLString), - description: 'Required argument description', - }, - // Add other arguments... - }, - type: GraphQLObjectTypeNewQuery, // or createCollectionType() for lists - resolve: resolveNewQuery, - }, -} -``` - -### 5. Register in Root Schema -In `rootSchema.ts`, add your resolver: - -```typescript -// Import your resolver -import { newQueryRoot } from './newQuery/newQueryResolver' - -// Add to the query fields -export const rootGraphQLSchema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'RootQueryType', - fields: { - ...introspectRoot, - ...searchRoot, - ...errorRoot, - ...newQueryRoot, // Add this line - }, - }), - types: [ - GraphQLObjectTypeGuide, - GraphQLObjectTypeReferenceCLICommand, - GraphQLObjectTypeReferenceSDKFunction, - GraphQLObjectTypeTroubleshooting, - ], -}) -``` - -### 6. Update TypeScript Types -Run the GraphQL codegen to update TypeScript types: -```bash -pnpm run -F docs codegen:graphql -``` - -## Best Practices - -1. **Error Handling**: Error handling always uses the Result class, defined in apps/docs/features/helpers.fn.ts -2. **Field Optimization**: Use `graphQLFields()` to only fetch requested data -3. **Collections**: Use `createCollectionType()` for paginated lists -4. **Naming**: Use `GRAPHQL_FIELD_*` constants for field names -5. **Documentation**: Add GraphQL descriptions to all fields and types -6. **Database**: Use `supabase()` client for database operations with `convertPostgrestToApiError` - -## Testing - -Tests are located in apps/docs/app/api/graphql/tests. Each top-level query -should have its own test file, located at .test.ts. - -### Test data - -Test data uses a local database, seeded with the file at supabase/seed.sql. Add -any data required for running your new query. - -### Integration tests - -Integration tests import the POST function defined in -apps/docs/api/graphql/route.ts, then make a request to this function. - -For example: - -```ts -import { POST } from '../route' - -it('test name', async () => { - const query = ` - query { - ... - } - ` - const request = new Request('http://localhost/api/graphql', { - method: 'POST', - body: JSON.stringify({ query }), - }) - - const result = await POST(request) -}) -``` - -Include at least the following tests: - -1. A test that requests all fields (including nested fields) on the new query - object, and asserts that there are no errors, and the requested fields are - properly returned. -2. A test that triggers and error, and asserts that a GraphQL error is properly - returned. diff --git a/.cursor/rules/docs-test-requirements.mdc b/.cursor/rules/docs-test-requirements.mdc deleted file mode 100644 index f76ca2ba7cff6..0000000000000 --- a/.cursor/rules/docs-test-requirements.mdc +++ /dev/null @@ -1,71 +0,0 @@ ---- -description: Docs Testing Procedure -globs: apps/docs/**/*.test.ts -alwaysApply: false ---- - -# Docs Test Requirements - -Rules for running tests in the docs application, ensuring proper Supabase setup and test execution. - - -name: docs_test_requirements -description: Standards for running tests in the docs application with proper Supabase setup -filters: - # Match test files in the docs app - - type: file_extension - pattern: "\\.(test|spec)\\.(ts|tsx)$" - - type: path - pattern: "^apps/docs/.*" - # Match test execution events - - type: event - pattern: "test_execution" - -actions: - - type: suggest - message: | - Before running tests in the docs app: - - 1. Check Supabase status: - ```bash - pnpm supabase status - ``` - - 2. If Supabase is not running: - ```bash - pnpm supabase start - ``` - - 3. Reset the database to ensure clean state: - ```bash - pnpm supabase db reset --local - ``` - - 4. Run the tests: - ```bash - pnpm run -F docs test:local:unwatch - ``` - - Important notes: - - Always ensure Supabase is running before tests - - Database must be reset to ensure clean state - - Use test:local:unwatch to run tests without watch mode - - Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx} - -examples: - - input: | - # Bad: Running tests without proper setup - pnpm run -F docs test - pnpm run -F docs test:local - - # Good: Proper test execution sequence - pnpm supabase status - pnpm supabase start # if not running - pnpm supabase db reset --local - pnpm run -F docs test:local:unwatch - output: "Correctly executed docs tests with proper Supabase setup" - -metadata: - priority: high - version: 1.0 - diff --git a/.cursor/rules/docs-embeddings-generation.md b/.cursor/rules/docs/docs-embeddings-generation/RULE.md similarity index 79% rename from .cursor/rules/docs-embeddings-generation.md rename to .cursor/rules/docs/docs-embeddings-generation/RULE.md index 22ce5fd4359ef..6eab6f71510d0 100644 --- a/.cursor/rules/docs-embeddings-generation.md +++ b/.cursor/rules/docs/docs-embeddings-generation/RULE.md @@ -1,3 +1,10 @@ +--- +description: "Docs: embeddings generation pipeline (apps/docs/scripts/search)" +globs: + - apps/docs/scripts/search/**/*.ts +alwaysApply: false +--- + # Documentation Embeddings Generation System ## Overview @@ -12,31 +19,34 @@ The documentation embeddings generation system processes various documentation s ## Architecture ### Main Entry Point -- `generate-embeddings.ts` - Main script that orchestrates the entire process + +- `apps/docs/scripts/search/generate-embeddings.ts` - Main script that orchestrates the entire process - Supports `--refresh` flag to force regeneration of all content ### Content Sources (`sources/` directory) #### Base Classes + - `BaseLoader` - Abstract class for loading content from different sources - `BaseSource` - Abstract class for processing and formatting content #### Source Types -1. **Markdown Sources** (`markdown.ts`) + +1. **Markdown Sources** (`apps/docs/scripts/search/sources/markdown.ts`) - Processes `.mdx` files from guides and documentation - Extracts frontmatter metadata and content sections -2. **Reference Documentation** (`reference-doc.ts`) +2. **Reference Documentation** (`apps/docs/scripts/search/sources/reference-doc.ts`) - **OpenAPI References** - Management API documentation from OpenAPI specs - **Client Library References** - JavaScript, Dart, Python, C#, Swift, Kotlin SDKs - **CLI References** - Command-line interface documentation - Processes YAML/JSON specs and matches with common sections -3. **GitHub Discussions** (`github-discussion.ts`) +3. **GitHub Discussions** (`apps/docs/scripts/search/sources/github-discussion.ts`) - Fetches troubleshooting discussions from GitHub using GraphQL API - Uses GitHub App authentication for access -4. **Partner Integrations** (`partner-integrations.ts`) +4. **Partner Integrations** (`apps/docs/scripts/search/sources/partner-integrations.ts`) - Fetches approved partner integration documentation from Supabase database - Technology integrations only (excludes agencies) @@ -56,4 +66,3 @@ The documentation embeddings generation system processes various documentation s - **`page`** table: Stores page metadata, content, checksum, version - **`page_section`** table: Stores individual sections with embeddings, token counts - diff --git a/.cursor/rules/docs/docs-graphql/RULE.md b/.cursor/rules/docs/docs-graphql/RULE.md new file mode 100644 index 0000000000000..8b16f176dc448 --- /dev/null +++ b/.cursor/rules/docs/docs-graphql/RULE.md @@ -0,0 +1,132 @@ +--- +description: "Docs: GraphQL architecture for apps/docs/resources" +globs: + - apps/docs/resources/**/*.ts +alwaysApply: false +--- + +# Docs GraphQL Architecture + +## Overview + +The `apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure. + +## Architecture Pattern + +Each GraphQL query follows this structure: + +``` +resources/ +├── queryObject/ +│ ├── queryObjectModel.ts # Data models and business logic +│ ├── queryObjectSchema.ts # GraphQL type definitions +│ ├── queryObjectResolver.ts # Query resolver and arguments +│ ├── queryObjectTypes.ts # TypeScript interfaces (optional) +│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional) +├── utils/ +│ ├── connections.ts # GraphQL connection/pagination utilities +│ └── fields.ts # GraphQL field selection utilities +├── rootSchema.ts # Main GraphQL schema with all queries +└── rootSync.ts # Root sync script for syncing to database +``` + +## Example queries + +1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content +2. **error** (`error/`) - Error code lookup for Supabase services +3. **schema** - GraphQL schema introspection + +## Key Files + +### `rootSchema.ts` + +- Main GraphQL schema definition +- Imports all resolvers and combines them into the root query +- Defines the `RootQueryType` with all top-level fields + +### `utils/connections.ts` + +- Provides `createCollectionType()` for paginated collections +- `GraphQLCollectionBuilder` for building collection responses +- Standard pagination arguments and edge/node patterns + +### `utils/fields.ts` + +- `graphQLFields()` utility to analyze requested fields in resolvers +- Used for optimizing data fetching based on what fields are actually requested + +## Creating a New Top-Level Query + +To add a new GraphQL query, follow these steps: + +### 1. Create Query Folder Structure + +```bash +mkdir resources/newQuery +touch resources/newQuery/newQueryModel.ts +touch resources/newQuery/newQuerySchema.ts +touch resources/newQuery/newQueryResolver.ts +``` + +### 2. Define GraphQL Schema (`newQuerySchema.ts`) + +```typescript +import { GraphQLObjectType, GraphQLString } from 'graphql' + +export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const + +export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({ + name: 'NewQuery', + description: 'Description of what this query returns', + fields: { + id: { + type: GraphQLString, + description: 'Unique identifier', + }, + // Add other fields... + }, +}) +``` + +### 3. Create Data Model (`newQueryModel.ts`) + +> [!NOTE] +> The data model should be agnostic to GraphQL. It may import argument types +> from `~/__generated__/graphql`, but otherwise all functions and classes +> should be unaware of whether they are called for GraphQL resolution. + +> [!TIP] +> The types in `~/__generated__/graphql` for a new endpoint will not exist +> until the code generation is run in the next step. + +```typescript +import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql' +import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils' +import { Result } from '~/features/helpers.fn' +import { supabase } from '~/lib/supabase' + +export class NewQueryModel { + constructor( + public readonly data: { + id: string + // other properties... + } + ) {} + + static async loadData( + args: RootQueryTypeNewQueryArgs, + requestedFields: Array + ): Promise> { + // Implement data fetching logic + const result = new Result( + await supabase() + .from('your_table') + .select('*') + // Add filters based on args + ) + .map((data) => data.map((item) => new NewQueryModel(item))) + .mapError(convertPostgrestToApiError) + return result + } +} +``` diff --git a/.cursor/rules/docs/docs-test-requirements/RULE.md b/.cursor/rules/docs/docs-test-requirements/RULE.md new file mode 100644 index 0000000000000..15cbda674e86c --- /dev/null +++ b/.cursor/rules/docs/docs-test-requirements/RULE.md @@ -0,0 +1,25 @@ +--- +description: "Docs: how to run tests locally (Supabase setup + correct commands)" +globs: + - apps/docs/**/*.{test,spec}.{ts,tsx} +alwaysApply: false +--- + +# Docs test requirements + +Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state. + +## Recommended sequence + +```bash +pnpm supabase status +pnpm supabase start # if not running +pnpm supabase db reset --local +pnpm run -F docs test:local:unwatch +``` + +## Notes + +- Always reset the local DB before running docs tests to avoid state leakage. +- Prefer `test:local:unwatch` for non-watch CI-like runs. + diff --git a/.cursor/rules/studio-ui.mdc b/.cursor/rules/studio-ui.mdc deleted file mode 100644 index 754cca1520b8f..0000000000000 --- a/.cursor/rules/studio-ui.mdc +++ /dev/null @@ -1,409 +0,0 @@ ---- -description: How to generate pages and interfaces in Studio, a web interface for managing Supabase projects -globs: -alwaysApply: true ---- - -## Project Structure - -- Next.js app using pages router -- Pages go in @apps/studio/pages - - Project related pages go in @apps/studio/pages/projects/[ref] - - Organization related pages go in @apps/studio/pages/org/[slug] -- Studio specific components go in @apps/studio/components - - Studio specific generic UI components go in @apps/studio/components/ui - - Studio specific components related to individual pages go in @apps/studio/components/interfaces e.g. @apps/studio/components/interfaces/Auth -- Generic helper functions go in @apps/studio/lib -- Generic hooks go in @apps/studio/hooks - -## Component system - -Our primitive component system is in @packages/ui and is based off shadcn/ui components. These components can be shared across all @apps e.g. studio and docs. Do not introduce new ui components unless asked to. - -- UI components are imported from this package across apps e.g. import { Button, Badge } from 'ui' -- Some components have a _Shadcn_ namespace appended to component name e.g. import { Input*Shadcn* } from 'ui' -- We should be using _Shadcn_ components where possible -- Before composing interfaces, read @packages/ui/index.tsx file for a full list of available components - -## Styling - -We use Tailwind for styling. - -- You should never use tailwind classes for colours and instead use classes we've defined ourselves - - Backgrounds // most of the time you will not need to define a background - - 'bg' used for main app surface background - - 'bg-muted' for elevating content // you can use Card instead - - 'bg-warning' for highlighting information that needs to be acted on - - 'bg-destructive' for highlighting issues - - Text - - 'text-foreground' for primary text like headings - - 'text-foreground-light' for body text - - 'text-foreground-lighter' for subtle text - - 'text-warning' for calling out information that needs action - - 'text-destructive' for calling out when something went wrong -- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium" -- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles - -## Page structure - -When creating a new page follow these steps: - -- Create the page in @apps/studio/pages -- Use the PageLayout component that has the following props - - ```jsx - export interface NavigationItem { - id?: string - label: string - href?: string - icon?: ReactNode - onClick?: () => void - badge?: string - active?: boolean - } - - interface PageLayoutProps { - children?: ReactNode - title?: string | ReactNode - subtitle?: string | ReactNode - icon?: ReactNode - breadcrumbs?: Array<{ - label?: string - href?: string - element?: ReactNode - }> - primaryActions?: ReactNode - secondaryActions?: ReactNode - navigationItems?: NavigationItem[] - className?: string - size?: 'default' | 'full' | 'large' | 'small' - isCompact?: boolean - } - ``` - -- If a page has page related actions, add them to primary and secondary action props e.g. Users page has "Create new user" action -- If a page is within an existing section (e.g. Auth), you should use the related layout component e.g. AuthLayout -- Create a new component in @apps/studio/components/interfaces for the contents of the page -- Use ScaffoldContainer if the page should be center aligned in a container -- Use ScaffoldSection, ScaffoldSectionTitle, ScaffoldSectionDescription if the page has multiple sections - -### Page example - -```jsx -import { MyPageComponent } from 'components/interfaces/MyPage/MyPageComponent' -import AuthLayout from './AuthLayout' -import DefaultLayout from 'components/layouts/DefaultLayout' -import { ScaffoldContainer } from 'components/layouts/Scaffold' -import type { NextPageWithLayout } from 'types' - -const MyPage: NextPageWithLayout = () => { - return ( - - - - ) -} - -MyPage.getLayout = (page) => ( - - {page} - -) - -export default MyPage - -export const MyPageComponent = () => ( - -
- My page section - A brief description of the purpose of the page -
- // Content goes here -
-) -``` - -## Forms - -Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels. - -### Core Principles - -- Build forms with `react-hook-form` + `zod` -- Always use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription` -- Always wrap form inputs with `FormControl_Shadcn_` to ensure proper form integration -- Keep imports from `ui` with `_Shadcn_` suffixes -- Handle dirty state: Show cancel buttons and disable save buttons based on `form.formState.isDirty` -- Show loading states on submit buttons using the `loading` prop -- If the submit button is outside the form, add a `formId` variable outside the component, set it as `id` on the form element and `form` prop on the button - -### Layout Selection - -- **Page layouts**: Use `FormItemLayout` with `layout="flex-row-reverse"` for horizontal alignment. Forms should be wrapped in a `Card` with each form field in its own `CardContent`, and `CardFooter` for actions. The layout automatically handles consistent input widths (50% on md, 40% on xl, min-w-100). -- **Side panels (wide)**: Use `FormItemLayout` with `layout="horizontal"`. Use `SheetSection` to wrap each field group. -- **Side panels (narrow, size="sm" or below)**: Use `FormItemLayout` with `layout="vertical"` - -### Page Layout Form Example - -```tsx -import { zodResolver } from '@hookform/resolvers/zod' -import { useForm } from 'react-hook-form' -import * as z from 'zod' - -import { - Button, - Card, - CardContent, - CardFooter, - Form_Shadcn_, - FormField_Shadcn_, - FormControl_Shadcn_, - Input_Shadcn_, - Switch, -} from 'ui' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -const formSchema = z.object({ - name: z.string().min(1, 'Name is required'), - enableFeature: z.boolean(), -}) - -export function SettingsForm() { - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { name: '', enableFeature: false }, - mode: 'onSubmit', - reValidateMode: 'onBlur', - }) - - function onSubmit(values: z.infer) { - // handle mutation with onSuccess/onError toast - } - - return ( - -
- - - ( - - - - - - )} - /> - - - ( - - - - - - )} - /> - - - {form.formState.isDirty && ( - - )} - - - -
-
- ) -} -``` - -### Side Panel Form Example - -```tsx -import { zodResolver } from '@hookform/resolvers/zod' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import * as z from 'zod' - -import { - Button, - Form_Shadcn_, - FormField_Shadcn_, - FormControl_Shadcn_, - Input_Shadcn_, - Sheet, - SheetContent, - SheetFooter, - SheetHeader, - SheetSection, - SheetTitle, -} from 'ui' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -const formSchema = z.object({ - name: z.string().min(1, 'Name is required'), -}) - -const formId = 'sidepanel-form' - -export function CreateResourcePanel() { - const [open, setOpen] = useState(false) - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { name: '' }, - }) - - function onSubmit(values: z.infer) { - // handle mutation - setOpen(false) - } - - return ( - - - - Create Resource - - -
- - ( - - - - - - )} - /> - -
-
- - - - -
-
- ) -} -``` - -### Common Form Field Types - -- **Text Input**: `Input_Shadcn_` with `placeholder` -- **Password Input**: `Input_Shadcn_` with `type="password"` -- **Number Input**: `Input_Shadcn_` with `type="number"` and `onChange={(e) => field.onChange(Number(e.target.value))}` -- **Input with Units**: Wrap `Input_Shadcn_` with `PrePostTab` component: `` -- **Textarea**: `Textarea` component with `rows` and `className="resize-none"` -- **Switch**: `Switch` with `checked={field.value} onCheckedChange={field.onChange}` -- **Checkbox**: `Checkbox_Shadcn_` with label, use multiple for checkbox groups -- **Select**: `Select_Shadcn_` with `SelectTrigger_Shadcn_`, `SelectContent_Shadcn_`, `SelectItem_Shadcn_` -- **Multi-Select**: Use `MultiSelector` from `ui-patterns/multi-select` -- **Radio Group**: `RadioGroupStacked` with `RadioGroupStackedItem` for stacked options with descriptions -- **Date Picker**: `Calendar` inside `Popover_Shadcn_` with a trigger button -- **Copyable Input**: Use `Input` from `ui-patterns/DataInputs/Input` with `copy` and `readOnly` props -- **Field Array**: Use `useFieldArray` from `react-hook-form` for dynamic add/remove fields -- **Action Field**: Use `FormItemLayout` without form control, just buttons for navigation or performable actions. Wrap buttons in a div with `justify-end` to align them to the right - -## Cards - -- Use cards when needing to group related pieces of information -- Cards can have sections with CardContent -- Use CardFooter for actions -- Only use CardHeader and CardTitle if the card content has not been described by the surrounding content e.g. Page title or ScaffoldSectionTitle -- Use CardHeader and CardTitle when you are using multiple Cards to group related pieces of content e.g. Primary branch, Persistent branches, Preview branches - -## Sheets - -- Use a sheet when needing to reveal more complicated forms or information relating to an object and context switching away to a new page would be disruptive e.g. we list auth providers, clicking an auth provider opens a sheet with information about that provider and a form to enable, user can close sheet to go back to providers list -- Use `SheetContent` with `size="lg"` for forms that need horizontal layout -- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure -- Place submit/cancel buttons in `SheetFooter` -- For forms in sheets, use `FormItemLayout` with `layout="horizontal"` for wider panels or `layout="vertical"` for narrow panels (size="sm" or below) -- See the Forms section for a complete side panel form example - -## React Query - -- When doing a mutation, always use the mutate function. Always use onSuccess and onError with a toast.success and toast.error. -- Use mutateAsync only if the mutation is part of multiple async actions. Wrap the mutateAsync call with try/catch block and add toast.success and toast.error. - -## Tables - -- Use the generic ui table components for most tables -- Tables are generally contained witin a card -- If a table has associated actions, they should go above on right hand side -- If a table has associated search or filters, they should go above on left hand side -- If a table is the main content of a page, and it does not have search or filters, you can add table actions to primary and secondary actions of PageLayout -- If a table is the main content of a page section, and it does not have search or filters, you can add table actions to the right of ScaffoldSectionTitle -- For simple lists of objects you can use ResourceList with ResourceListItem instead - -### Table example - -```jsx -import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui' -; - A list of your recent invoices. - - - Invoice - Status - Method - Amount - - - - - INV001 - Paid - Credit Card - $250.00 - - -
-``` - -## Alerts - -- Use Admonition component to alert users of important actions or restrictions in place -- Place the Admonition either at the top of the contents of the page (below page title) or at the top of the related ScaffoldSection , below ScaffoldTitle -- Use sparingly - -### Alert example - -```jsx - -``` diff --git a/.cursor/rules/studio/RULE.md b/.cursor/rules/studio/RULE.md new file mode 100644 index 0000000000000..60e3c1558024a --- /dev/null +++ b/.cursor/rules/studio/RULE.md @@ -0,0 +1,33 @@ +--- +description: 'Studio: index rule for architecture, style, and UI composition patterns' +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio + +Use the nested rules in this folder for focused guidance while working in `apps/studio/`. + +## Architecture and style + +- `studio/project-structure` +- `studio/component-system` +- `studio/styling` +- `studio/best-practices` + +## UI composition (Design System patterns) + +- `studio/layout` +- `studio/forms` +- `studio/tables` +- `studio/charts` +- `studio/empty-states` +- `studio/navigation` + +## Common UI building blocks + +- `studio/sheets` +- `studio/cards` +- `studio/alerts` +- `studio/react-query` diff --git a/.cursor/rules/studio/alerts/RULE.md b/.cursor/rules/studio/alerts/RULE.md new file mode 100644 index 0000000000000..feb55b0564b4c --- /dev/null +++ b/.cursor/rules/studio/alerts/RULE.md @@ -0,0 +1,13 @@ +--- +description: "Studio: alert/admonition usage and placement" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio alerts + +- Use `Admonition` to call out important actions, restrictions, or critical context. +- Place at the top of a page’s content (below the page title) or at the top of the relevant section (below the section title). +- Use sparingly. + diff --git a/.cursor/rules/studio-best-practices.mdc b/.cursor/rules/studio/best-practices/RULE.md similarity index 97% rename from .cursor/rules/studio-best-practices.mdc rename to .cursor/rules/studio/best-practices/RULE.md index ebb06fc7e1f88..357265450a842 100644 --- a/.cursor/rules/studio-best-practices.mdc +++ b/.cursor/rules/studio/best-practices/RULE.md @@ -1,9 +1,8 @@ --- -description: React best practices and coding standards for Studio +description: "Studio: React and TypeScript best practices for maintainable Studio code" globs: - - apps/studio/**/*.tsx - - apps/studio/**/*.ts -alwaysApply: true + - apps/studio/**/*.{ts,tsx} +alwaysApply: false --- # Studio Best Practices @@ -307,15 +306,15 @@ const Component = ({ onClose, onSave }: Props) => { ```tsx // ❌ Bad - creates new function every render - handleItemClick(item)} -/> + handleItemClick(item)} /> // ✅ Good - stable reference with useCallback -const handleItemClick = useCallback((item: Item) => { - // handle click -}, [dependencies]) +const handleItemClick = useCallback( + (item: Item) => { + // handle click + }, + [dependencies] +) ``` diff --git a/.cursor/rules/studio/cards/RULE.md b/.cursor/rules/studio/cards/RULE.md new file mode 100644 index 0000000000000..efcb36733b16b --- /dev/null +++ b/.cursor/rules/studio/cards/RULE.md @@ -0,0 +1,14 @@ +--- +description: "Studio: Card usage for grouping related content and actions" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio cards + +- Use cards to group related pieces of information. +- Use `CardContent` for sections and `CardFooter` for actions. +- Only use `CardHeader`/`CardTitle` when the card content is not already described by surrounding content (page title, section title, etc). +- Prefer headers/titles when multiple cards represent distinct groups (e.g. multiple settings groups). + diff --git a/.cursor/rules/studio/charts/RULE.md b/.cursor/rules/studio/charts/RULE.md new file mode 100644 index 0000000000000..558580db3e81e --- /dev/null +++ b/.cursor/rules/studio/charts/RULE.md @@ -0,0 +1,26 @@ +--- +description: "Studio: composable chart patterns built on Recharts and our chart presentational components" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio charts + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/charts.mdx` +- Demos: + - `apps/design-system/__registry__/default/block/chart-composed-demo.tsx` + - `apps/design-system/__registry__/default/block/chart-composed-basic.tsx` + - `apps/design-system/__registry__/default/block/chart-composed-states.tsx` + - `apps/design-system/__registry__/default/block/chart-composed-metrics.tsx` + - `apps/design-system/__registry__/default/block/chart-composed-actions.tsx` + - `apps/design-system/__registry__/default/block/chart-composed-table.tsx` + +## Best practices + +- Prefer provided chart building blocks over passing raw Recharts components to `ChartContent`. +- Use `useChart` context flags for consistent loading/disabled handling. +- Keep chart composition straightforward; avoid over-abstraction. + diff --git a/.cursor/rules/studio/component-system/RULE.md b/.cursor/rules/studio/component-system/RULE.md new file mode 100644 index 0000000000000..b6433123d0d9c --- /dev/null +++ b/.cursor/rules/studio/component-system/RULE.md @@ -0,0 +1,16 @@ +--- +description: 'Studio: UI component system (packages/ui + shadcn primitives)' +globs: + - apps/studio/**/*.{ts,tsx} + - packages/ui/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio component system + +Our primitive component system lives in `packages/ui` and is based on shadcn/ui patterns. + +- Prefer using components exported from `ui` (e.g. `import { Button } from 'ui'`). +- Prefer `_Shadcn_`-suffixed components for form components e.g. `Input_Shadcn_`. +- Avoid introducing new primitives unless explicitly requested. +- Browse available exports in `packages/ui/index.tsx` before composing new UI. diff --git a/.cursor/rules/studio/empty-states/RULE.md b/.cursor/rules/studio/empty-states/RULE.md new file mode 100644 index 0000000000000..266213fe9a945 --- /dev/null +++ b/.cursor/rules/studio/empty-states/RULE.md @@ -0,0 +1,25 @@ +--- +description: 'Studio: empty state patterns (presentational vs informational vs zero-results vs missing route)' +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio empty states + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/empty-states.mdx` +- Demos: + - `apps/design-system/registry/default/example/empty-state-presentational-icon.tsx` + - `apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx` + - `apps/design-system/registry/default/example/empty-state-zero-items-table.tsx` + - `apps/design-system/registry/default/example/data-grid-empty-state.tsx` + - `apps/design-system/registry/default/example/empty-state-missing-route.tsx` + +## Quick guidance + +- Initial states: use presentational empty states when onboarding/value prop + a clear next action helps. +- Data-heavy lists: prefer informational empty states that match the list/table layout. +- Zero results: keep the UI consistent with the data state to avoid jarring transitions. +- Missing routes: prefer a centered `Admonition` pattern. diff --git a/.cursor/rules/studio/forms/RULE.md b/.cursor/rules/studio/forms/RULE.md new file mode 100644 index 0000000000000..b9b729111b5dc --- /dev/null +++ b/.cursor/rules/studio/forms/RULE.md @@ -0,0 +1,35 @@ +--- +description: "Studio: form patterns (page layouts + side panels) and react-hook-form conventions" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio forms + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/forms.mdx` +- Demos: + - `apps/design-system/registry/default/example/form-patterns-pagelayout.tsx` + - `apps/design-system/registry/default/example/form-patterns-sidepanel.tsx` + +## Requirements + +- Build forms with `react-hook-form` + `zod`. +- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`. +- Wrap inputs with `FormControl_Shadcn_`. +- Use `_Shadcn_` imports from `ui` for form primitives where available. + +## Layout selection + +- Page layouts: `FormItemLayout layout="flex-row-reverse"` inside `Card` (`CardContent` per field; `CardFooter` for actions). +- Side panels (wide): `FormItemLayout layout="horizontal"` inside `SheetSection`. +- Side panels (narrow, `size="sm"` or below): `FormItemLayout layout="vertical"`. + +## Actions and state + +- Handle dirty state (`form.formState.isDirty`) to show Cancel and to disable Save. +- Show loading on submit buttons via `loading`. +- When submit button is outside the `
`, set a stable `formId` and use the button’s `form` prop. + diff --git a/.cursor/rules/studio/layout/RULE.md b/.cursor/rules/studio/layout/RULE.md new file mode 100644 index 0000000000000..b39f082b740de --- /dev/null +++ b/.cursor/rules/studio/layout/RULE.md @@ -0,0 +1,28 @@ +--- +description: 'Studio: page layout patterns (PageContainer/PageHeader/PageSection) and sizing guidance. Use to learn how to create or update existing pages in Studio.' +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio layout + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/layout.mdx` +- Demos: + - `apps/design-system/registry/default/example/page-layout-settings.tsx` + - `apps/design-system/registry/default/example/page-layout-list.tsx` + - `apps/design-system/registry/default/example/page-layout-list-simple.tsx` + - `apps/design-system/registry/default/example/page-layout-detail.tsx` + +## Guidelines + +- Build pages using `PageContainer`, `PageHeader`, and `PageSection` for consistent spacing and max-widths. +- Choose `size` based on content: + - Settings/config: `size="default"` + - List/table-heavy: `size="large"` + - Full-screen experiences: `size="full"` +- For list pages: + - If filters/search exist, align table actions with filters (avoid `PageHeaderAside`/`PageSectionAside` for those actions). + - If no filters/search, actions can go in `PageHeaderAside` or `PageSectionAside` depending on context. diff --git a/.cursor/rules/studio/navigation/RULE.md b/.cursor/rules/studio/navigation/RULE.md new file mode 100644 index 0000000000000..eb37afda3fb91 --- /dev/null +++ b/.cursor/rules/studio/navigation/RULE.md @@ -0,0 +1,19 @@ +--- +description: "Studio: navigation patterns (page-level NavMenu + URL-driven navigation)" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio navigation + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/navigation.mdx` + +## NavMenu + +- Use `NavMenu` for a horizontal list of related views within a consistent page layout. +- Activating an item should trigger a URL change (no local-only tab state). +- See: `apps/design-system/content/docs/components/nav-menu.mdx` + diff --git a/.cursor/rules/studio/project-structure/RULE.md b/.cursor/rules/studio/project-structure/RULE.md new file mode 100644 index 0000000000000..d87139dd5a3c7 --- /dev/null +++ b/.cursor/rules/studio/project-structure/RULE.md @@ -0,0 +1,19 @@ +--- +description: "Studio: project structure and where code lives" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio project structure + +- Studio is a Next.js app using the pages router. +- Pages live in `apps/studio/pages`. + - Project pages: `apps/studio/pages/projects/[ref]` + - Org pages: `apps/studio/pages/org/[slug]` +- Studio components live in `apps/studio/components`. + - Studio UI helpers: `apps/studio/components/ui` + - Interface/page components: `apps/studio/components/interfaces` (e.g. `apps/studio/components/interfaces/Auth`) +- Shared hooks: `apps/studio/hooks` +- Shared helpers: `apps/studio/lib` + diff --git a/.cursor/rules/studio/queries/RULE.md b/.cursor/rules/studio/queries/RULE.md new file mode 100644 index 0000000000000..0e394b01d28ae --- /dev/null +++ b/.cursor/rules/studio/queries/RULE.md @@ -0,0 +1,113 @@ +--- +description: 'Studio: data fetching conventions for queries/mutations (React Query hooks)' +globs: + - apps/studio/data/**/*.{ts,tsx} + - apps/studio/pages/**/*.{ts,tsx} + - apps/studio/components/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio queries & mutations (React Query) + +Follow the `apps/studio/data/` patterns used by edge functions: + +- Query hook: `apps/studio/data/edge-functions/edge-functions-query.ts` +- Mutation hook: `apps/studio/data/edge-functions/edge-functions-update-mutation.ts` +- Keys: `apps/studio/data/edge-functions/keys.ts` +- Page usage: `apps/studio/pages/project/[ref]/functions/index.tsx` + +## Organize query keys + +- Define a `keys.ts` per domain and export `*Keys` helpers (use array keys with `as const`). +- Do not inline query keys in components. + +Example: + +```ts +export const edgeFunctionsKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const, + detail: (projectRef: string | undefined, slug: string | undefined) => + ['projects', projectRef, 'edge-function', slug, 'detail'] as const, +} +``` + +## Write a query hook + +- Export `Variables`, `Data`, and `Error` types from the file. +- Implement a `getX(variables, signal?)` function that: + - throws if required variables are missing + - passes the `signal` through to the fetcher for cancellation + - calls `handleError(error)` and returns `data` +- Wrap it in `useXQuery()` using `useQuery`, `UseCustomQueryOptions`, and a domain key helper. +- Gate with `enabled` so the query doesn’t run until required variables exist (and platform-only queries should include `IS_PLATFORM`). + +Template: + +```ts +export type XVariables = { projectRef?: string } +export type XError = ResponseError + +export async function getX({ projectRef }: XVariables, signal?: AbortSignal) { + if (!projectRef) throw new Error('projectRef is required') + const { data, error } = await get('/v1/projects/{ref}/x', { + params: { path: { ref: projectRef } }, + signal, + }) + if (error) handleError(error) + return data +} + +export type XData = Awaited> + +export const useXQuery = ( + { projectRef }: XVariables, + { enabled = true, ...options }: UseCustomQueryOptions = {} +) => + useQuery({ + queryKey: xKeys.list(projectRef), + queryFn: ({ signal }) => getX({ projectRef }, signal), + enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined', + ...options, + }) +``` + +## Write a mutation hook + +- Export a `Variables` type that includes `projectRef`, identifiers (e.g. `slug`), and `payload`. +- Implement an `updateX(vars)` function that validates required variables and uses `handleError`. +- Prefer a `useXMutation()` wrapper that: + - accepts `UseCustomMutationOptions` (omit `mutationFn`) + - invalidates the relevant `list()` + `detail()` keys in `onSuccess` and `await`s them via `Promise.all` + - defaults to a `toast.error(...)` when `onError` isn’t provided + +Template: + +```ts +export const useXUpdateMutation = ({ onSuccess, onError, ...options } = {}) => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: updateX, + async onSuccess(data, variables, context) { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: xKeys.detail(variables.projectRef, variables.slug), + }), + queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }), + ]) + await onSuccess?.(data, variables, context) + }, + async onError(error, variables, context) { + if (onError === undefined) toast.error(`Failed to update: ${error.message}`) + else onError(error, variables, context) + }, + ...options, + }) +} +``` + +## Component usage + +- Prefer React Query’s v5 flags: + - `isPending` for initial load (often aliased to `isLoading`) + - `isFetching` for background refetches +- Render states explicitly (pending → error → success), like `apps/studio/pages/project/[ref]/functions/index.tsx`. diff --git a/.cursor/rules/studio/sheets/RULE.md b/.cursor/rules/studio/sheets/RULE.md new file mode 100644 index 0000000000000..f55b3958ec5e9 --- /dev/null +++ b/.cursor/rules/studio/sheets/RULE.md @@ -0,0 +1,23 @@ +--- +description: "Studio: side panels (Sheet) for context-preserving workflows" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio sheets + +Use a `Sheet` when switching to a new page would be disruptive and the user should keep context (e.g. selecting an item from a list to edit details). + +## Structure + +- Prefer `SheetContent` with `size="lg"` for forms that need horizontal layout. +- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure. +- Place submit/cancel actions in `SheetFooter`. + +## Forms in sheets + +- Prefer `FormItemLayout`: + - `layout="horizontal"` for wider sheets + - `layout="vertical"` for narrow sheets (`size="sm"` or below) +- See `@studio/forms` for the canonical patterns and demos. diff --git a/.cursor/rules/studio/styling/RULE.md b/.cursor/rules/studio/styling/RULE.md new file mode 100644 index 0000000000000..f1679fbc26450 --- /dev/null +++ b/.cursor/rules/studio/styling/RULE.md @@ -0,0 +1,16 @@ +--- +description: "Studio: styling rules (Tailwind + semantic tokens + typography/focus utilities)" +globs: + - apps/studio/**/*.{ts,tsx,scss} +alwaysApply: false +--- + +# Studio styling + +- Use Tailwind. +- Do not hardcode Tailwind color tokens; use our semantic classes: + - backgrounds: `bg`, `bg-muted`, `bg-warning`, `bg-destructive` + - text: `text-foreground`, `text-foreground-light`, `text-foreground-lighter`, `text-warning`, `text-destructive` +- Use existing typography utilities from `apps/studio/styles/typography.scss` instead of recreating styles. +- Use existing focus utilities from `apps/studio/styles/focus.scss` for consistent keyboard focus styling. + diff --git a/.cursor/rules/studio/tables/RULE.md b/.cursor/rules/studio/tables/RULE.md new file mode 100644 index 0000000000000..c610fe842681b --- /dev/null +++ b/.cursor/rules/studio/tables/RULE.md @@ -0,0 +1,29 @@ +--- +description: "Studio: table patterns (Table vs Data Table vs Data Grid) and placement of actions/filters" +globs: + - apps/studio/**/*.{ts,tsx} +alwaysApply: false +--- + +# Studio tables + +Use the Design System UI pattern docs as the source of truth: + +- Documentation: `apps/design-system/content/docs/ui-patterns/tables.mdx` +- Demos: + - `apps/design-system/registry/default/example/table-demo.tsx` + - `apps/design-system/registry/default/example/data-table-demo.tsx` + - `apps/design-system/registry/default/example/data-grid-demo.tsx` + +## Choose the right pattern + +- `Table`: simple, static, semantic table display. +- Data Table: TanStack-powered pattern for sorting/filtering/pagination; composed per use case. +- Data Grid: only when you need virtualization, column resizing, or complex cell editing. + +## Actions and filters placement + +- Actions: above the table, aligned right. +- Search/filters: above the table, aligned left. +- If the table is the primary page content and has no filters/search, actions can live in the page’s primary/secondary actions area. + diff --git a/.cursor/rules/e2e-testing.mdc b/.cursor/rules/testing/e2e-studio/RULE.md similarity index 98% rename from .cursor/rules/e2e-testing.mdc rename to .cursor/rules/testing/e2e-studio/RULE.md index 346ecf4b75d81..354cd86ac2025 100644 --- a/.cursor/rules/e2e-testing.mdc +++ b/.cursor/rules/testing/e2e-studio/RULE.md @@ -1,9 +1,9 @@ --- -description: E2E testing best practices for Playwright tests in Studio +description: "Testing: Playwright E2E best practices for Studio tests (avoid flake + race conditions)" globs: - e2e/studio/**/*.ts - e2e/studio/**/*.spec.ts -alwaysApply: true +alwaysApply: false --- # E2E Testing Best Practices diff --git a/.cursor/rules/testing/unit-integration/RULE.md b/.cursor/rules/testing/unit-integration/RULE.md new file mode 100644 index 0000000000000..7248ee737a462 --- /dev/null +++ b/.cursor/rules/testing/unit-integration/RULE.md @@ -0,0 +1,10 @@ +--- +description: "Testing: unit/integration conventions for Studio test files" +globs: + - apps/studio/**/*.test.ts + - apps/studio/**/*.test.tsx +alwaysApply: false +--- + +Follow the guidelines in `apps/studio/tests/README.md` when writing tests for Studio. + diff --git a/.cursor/rules/unit-integration-testing.mdc b/.cursor/rules/unit-integration-testing.mdc deleted file mode 100644 index e3a659a7c53dd..0000000000000 --- a/.cursor/rules/unit-integration-testing.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: -globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx -alwaysApply: false ---- -Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md) diff --git a/apps/design-system/content/docs/components/table.mdx b/apps/design-system/content/docs/components/table.mdx index c57ad66170c66..2de803e5266f0 100644 --- a/apps/design-system/content/docs/components/table.mdx +++ b/apps/design-system/content/docs/components/table.mdx @@ -199,11 +199,13 @@ Avoid adding other actions when using row-level navigation, as multiple interact -When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management, including: +When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances: -- Handling `Enter` and `Space` key presses for activation -- Providing visual focus indicators using classes like `inset-focus` -- Supporting modifier keys (`Ctrl`/`Cmd`) for opening links in new tabs +- Handle `Enter` and `Space` key presses for activation +- Provide visual focus indicators using classes like `inset-focus` +- Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs + - Consider using the shared `createNavigationHandler` function to handle modifier keys +- Avoid bubbling up action events from _within_ the row #### Row navigation with actions diff --git a/apps/design-system/registry/default/example/table-row-link-actions.tsx b/apps/design-system/registry/default/example/table-row-link-actions.tsx index ee82a507b7ed5..6acf355d8a265 100644 --- a/apps/design-system/registry/default/example/table-row-link-actions.tsx +++ b/apps/design-system/registry/default/example/table-row-link-actions.tsx @@ -22,11 +22,13 @@ const policies = [ }, ] +// Studio: See also createNavigationHandler in apps/studio/lib/navigation.ts +// It handles all of the below, plus modifier clicks and middle mouse button clicks. const handlePolicyNavigation = ( - bucketId: string, + policyId: string, event: React.MouseEvent | React.KeyboardEvent ) => { - const url = `/${bucketId}` + const url = `/${policyId}` if (event.metaKey || event.ctrlKey) { // window.open(`${url}`, '_blank') Disabled for demo purposes } else { diff --git a/apps/design-system/registry/default/example/table-row-link.tsx b/apps/design-system/registry/default/example/table-row-link.tsx index 2ea3ec1443669..be4716eff2494 100644 --- a/apps/design-system/registry/default/example/table-row-link.tsx +++ b/apps/design-system/registry/default/example/table-row-link.tsx @@ -20,6 +20,8 @@ const buckets = [ }, ] +// Studio: See also createNavigationHandler in apps/studio/lib/navigation.ts +// It handles all of the below, plus modifier clicks and middle mouse button clicks. const handleBucketNavigation = ( bucketId: string, event: React.MouseEvent | React.KeyboardEvent diff --git a/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx b/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx index f4fd73b4cb678..35d1db45cc6a3 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx @@ -45,7 +45,7 @@ hideToc: true ```bash name=Terminal - cd my-app && npx expo install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill + cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite ``` @@ -82,7 +82,7 @@ hideToc: true Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables. - The code below uses [AsyncStorage](https://www.npmjs.com/package/@react-native-async-storage/async-storage) to persist the user session in your app. + The code below uses Expo's localStorage polyfill to persist authentication sessions. @@ -91,14 +91,14 @@ hideToc: true ```ts name=lib/supabase.ts import 'react-native-url-polyfill/auto' import { createClient } from '@supabase/supabase-js' - import AsyncStorage from '@react-native-async-storage/async-storage' + import 'expo-sqlite/localStorage/install'; const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { - storage: AsyncStorage, + storage: localStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, diff --git a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx index 05fe900af0f6a..9d2cd33ca8511 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx @@ -34,7 +34,7 @@ cd expo-user-management Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js) ```bash -npx expo install @supabase/supabase-js @react-native-async-storage/async-storage @rneui/themed +npx expo install @supabase/supabase-js @rneui/themed expo-sqlite ``` Now let's create a helper file to initialize the Supabase client. @@ -46,15 +46,15 @@ These variables are safe to expose in your Expo app since Supabase has scrollable size="large" type="underlined" - defaultActiveId="async-storage" + defaultActiveId="local-storage" queryGroup="auth-store" > - + <$CodeTabs> ```ts name=lib/supabase.ts - import AsyncStorage from '@react-native-async-storage/async-storage' + import 'expo-sqlite/localStorage/install'; import { createClient } from '@supabase/supabase-js' const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL @@ -62,7 +62,7 @@ These variables are safe to expose in your Expo app since Supabase has export const supabase = createClient(supabaseUrl, supabasePublishableKey, { auth: { - storage: AsyncStorage, + storage: localStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, diff --git a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx index 289228f706804..129cf6440be4e 100644 --- a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx +++ b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx @@ -76,7 +76,7 @@ function BaseEditor({ {value === null ? 'NULL' : value} - + { tooltip={{ content: { side: 'bottom', text: 'View referencing record' } }} /> - + { +export const FilterPopover = () => { const { urlFilters, onApplyFilters } = useTableFilter() // Convert string[] to Filter[] @@ -16,7 +12,5 @@ export const FilterPopover = ({ portal = true }: FilterPopoverProps) => { return formatFilterURLParams(urlFilters ?? []) }, [urlFilters]) - return ( - - ) + return } diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx index 868b2e2715ef6..a3e4eb1c3297e 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx @@ -17,14 +17,12 @@ export interface FilterPopoverPrimitiveProps { buttonText?: string filters: Filter[] onApplyFilters: (filters: Filter[]) => void - portal?: boolean } export const FilterPopoverPrimitive = ({ buttonText, filters, onApplyFilters, - portal = true, }: FilterPopoverPrimitiveProps) => { const [open, setOpen] = useState(false) const snap = useTableEditorTableStateSnapshot() @@ -94,7 +92,7 @@ export const FilterPopoverPrimitive = ({ {displayButtonText} - +
{localFilters.map((filter, index) => ( diff --git a/apps/studio/components/grid/components/header/sort/SortPopover.tsx b/apps/studio/components/grid/components/header/sort/SortPopover.tsx index 70afe576c34e0..cc3cc715324c5 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopover.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopover.tsx @@ -6,11 +6,10 @@ import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { SortPopoverPrimitive } from './SortPopoverPrimitive' export interface SortPopoverProps { - portal?: boolean tableQueriesEnabled?: boolean } -export const SortPopover = ({ portal = true, tableQueriesEnabled }: SortPopoverProps) => { +export const SortPopover = ({ tableQueriesEnabled }: SortPopoverProps) => { const { urlSorts, onApplySorts } = useTableSort() const snap = useTableEditorTableStateSnapshot() @@ -23,7 +22,6 @@ export const SortPopover = ({ portal = true, tableQueriesEnabled }: SortPopoverP return ( void - portal?: boolean defaultOpen?: boolean tableQueriesEnabled?: boolean } @@ -48,7 +47,6 @@ export const SortPopoverPrimitive = ({ buttonText, sorts, onApplySorts, - portal = true, defaultOpen = false, tableQueriesEnabled = true, }: SortPopoverPrimitiveProps) => { @@ -225,7 +223,7 @@ export const SortPopoverPrimitive = ({ {displayButtonText} - +
{localSorts.map((sort, index) => ( { queryParams={{ category: SupportCategories.PROBLEM, subject: 'Help with API keys', - message: - "I'm experiencing problems with the new API keys feature. Please describe your specific issue here.", }} > Contact support diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index d4a0e6ac0e806..9c51aa717b587 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -360,7 +360,6 @@ export const CreateHookSheet = ({ > - + diff --git a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx index 5ae5341f272df..c8116b201ae72 100644 --- a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx @@ -1,15 +1,17 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Check, Github, Loader2 } from 'lucide-react' import Image from 'next/image' -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' +import { useRouter } from 'next/router' +import { useCallback, useEffect, useState } from 'react' +import { useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' +import { InlineLink } from '@/components/ui/InlineLink' +import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import AlertError from 'components/ui/AlertError' +import { AlertError } from 'components/ui/AlertError' import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation' import { Branch, useBranchesQuery } from 'data/branches/branches-query' import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query' @@ -17,7 +19,6 @@ import { useGitHubConnectionsQuery } from 'data/integrations/github-connections- import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' -import { useRouter } from 'next/router' import { Badge, Button, @@ -30,7 +31,6 @@ import { DialogTitle, FormControl_Shadcn_, FormField_Shadcn_, - FormMessage_Shadcn_, Form_Shadcn_, Input_Shadcn_, Label_Shadcn_ as Label, @@ -69,10 +69,9 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro }) const { data: branches } = useBranchesQuery({ projectRef }) - const { mutateAsync: checkGithubBranchValidity, isPending: isChecking } = - useCheckGithubBranchValidity({ - onError: () => {}, - }) + const { mutate: checkGithubBranchValidity, isPending: isChecking } = useCheckGithubBranchValidity( + { onError: () => {} } + ) const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({ onSuccess: (data) => { @@ -88,63 +87,48 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? [] const formId = 'edit-branch-form' - const FormSchema = z - .object({ - branchName: z - .string() - .min(1, 'Branch name cannot be empty') - .refine( - (val) => /^[a-zA-Z0-9\-_]+$/.test(val), - 'Branch name can only contain alphanumeric characters, hyphens, and underscores.' - ) - .refine( - (val) => - // Allow the current branch name during edit - val === branch?.name || (branches ?? []).every((b) => b.name !== val), - 'A branch with this name already exists' - ), - gitBranchName: z - .string() - .refine( - (val) => gitlessBranching || !githubConnection || (val && val.length > 0), - 'Git branch name is required when GitHub is connected' - ), - }) - .superRefine(async (val, ctx) => { - if (val.gitBranchName && val.gitBranchName.length > 0 && githubConnection?.repository.id) { - try { - await checkGithubBranchValidity({ - repositoryId: githubConnection.repository.id, - branchName: val.gitBranchName, - }) - setIsGitBranchValid(true) - } catch (error) { - setIsGitBranchValid(false) - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Unable to find branch "${val.gitBranchName}" in ${repoOwner}/${repoName}`, - path: ['gitBranchName'], - }) - } - } else { - // If git branch is empty or removed, it's valid for gitless branching - setIsGitBranchValid( - !val.gitBranchName || val.gitBranchName.length === 0 || gitlessBranching - ) - } - }) + const FormSchema = z.object({ + branchName: z + .string() + .min(1, 'Branch name cannot be empty') + .refine( + (val) => /^[a-zA-Z0-9\-_]+$/.test(val), + 'Branch name can only contain alphanumeric characters, hyphens, and underscores.' + ) + .refine( + (val) => + // Allow the current branch name during edit + val === branch?.name || (branches ?? []).every((b) => b.name !== val), + 'A branch with this name already exists' + ), + gitBranchName: z + .string() + .refine( + (val) => gitlessBranching || !githubConnection || (val && val.length > 0), + 'Git branch name is required when GitHub is connected' + ), + }) const form = useForm>({ - mode: 'onBlur', + mode: 'onChange', reValidateMode: 'onChange', resolver: zodResolver(FormSchema), defaultValues: { branchName: '', gitBranchName: '' }, }) + const gitBranchName = useWatch({ control: form.control, name: 'gitBranchName' }) + const debouncedGitBranchName = useDebounce(gitBranchName, 500) - const isFormValid = - form.formState.isValid && (!form.getValues('gitBranchName') || isGitBranchValid) + const isFormValid = form.formState.isValid && (!gitBranchName || isGitBranchValid) const canSubmit = isFormValid && !isUpdating && !isChecking + const openLinkerPanel = () => { + onClose() + + if (projectRef) { + router.push(`/project/${projectRef}/settings/integrations`) + } + } + const onSubmit = (data: z.infer) => { if (!projectRef) return console.error('Project ref is required') if (!branch?.project_ref) return console.error('Branch ref is required') @@ -169,6 +153,37 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro updateBranch(payload) } + const validateGitBranchName = useCallback( + (branchName: string) => { + if (!githubConnection) + return console.error( + '[EditBranchModal > validateGitBranchName] GitHub Connection is missing' + ) + + const repositoryId = githubConnection.repository.id + const requested = branchName + checkGithubBranchValidity( + { repositoryId, branchName }, + { + onSuccess: () => { + if (form.getValues('gitBranchName') !== requested) return + setIsGitBranchValid(true) + form.clearErrors('gitBranchName') + }, + onError: (error) => { + if (form.getValues('gitBranchName') !== requested) return + setIsGitBranchValid(false) + form.setError('gitBranchName', { + ...error, + message: `Unable to find branch "${branchName}" in ${repoOwner}/${repoName}`, + }) + }, + } + ) + }, + [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName] + ) + // Pre-fill form when the modal becomes visible and branch data is available useEffect(() => { if (visible && branch) { @@ -180,30 +195,16 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro } }, [branch, visible, form, gitlessBranching]) - // Handle initial state and changes for git branch validity useEffect(() => { - setIsGitBranchValid( - !form.getValues('gitBranchName') || - form.getValues('gitBranchName')?.length === 0 || - gitlessBranching - ) - // Trigger validation if a git branch name exists initially or is entered - if (form.getValues('gitBranchName')) { - form.trigger('gitBranchName') + if (!githubConnection || !debouncedGitBranchName) { + setIsGitBranchValid(gitlessBranching) + form.clearErrors('gitBranchName') + return } - }, [ - githubConnection?.id, - form.getValues('gitBranchName'), - form.trigger, - visible, - branch, - gitlessBranching, - ]) - const openLinkerPanel = () => { - onClose() - router.push(`/project/${projectRef}/settings/integrations`) - } + form.clearErrors('gitBranchName') + validateGitBranchName(debouncedGitBranchName) + }, [debouncedGitBranchName, validateGitBranchName, form, githubConnection, gitlessBranching]) return ( !open && onClose()}> @@ -250,14 +251,9 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro height={16} alt={`GitHub icon`} /> - + {repoOwner}/{repoName} - +
} @@ -273,16 +269,22 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro {...field} placeholder="e.g. main, feat/some-feature" autoComplete="off" + onChange={(e) => { + field.onChange(e) + setIsGitBranchValid(false) + }} /> -
- {isChecking && } - {field.value && !isChecking && isGitBranchValid && ( - - )} +
+ {field.value ? ( + isChecking ? ( + + ) : isGitBranchValid ? ( + + ) : null + ) : null}
- )} /> diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 130a129a3df98..b59b6e0a6e144 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -300,7 +300,7 @@ export const DatabaseConnectionString = () => {
- + diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index 095acb0f487fd..b28b5eb7221e6 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -203,7 +203,6 @@ export const CreateFunction = ({ > s.name)} size="small" diff --git a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 6db649328cf52..8d762cc918d08 100644 --- a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -44,7 +44,7 @@ interface CreateIndexSidePanelProps { onClose: () => void } -const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => { +export const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => { const { data: project } = useSelectedProjectQuery() const isOrioleDb = useIsOrioleDb() @@ -420,5 +420,3 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp ) } - -export default CreateIndexSidePanel diff --git a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx index e463dcb481140..8188e9c224be4 100644 --- a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -30,7 +30,7 @@ import { Input } from 'ui-patterns/DataInputs/Input' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' -import CreateIndexSidePanel from './CreateIndexSidePanel' +import { CreateIndexSidePanel } from './CreateIndexSidePanel' const Indexes = () => { const { data: project } = useSelectedProjectQuery() diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index fb76465159289..6b01b6f1dd6a1 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -235,7 +235,7 @@ export const TableList = ({ icon={} /> - +

Show entity types

diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx index 7819382c6fabf..b8853cad5e9e9 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx @@ -428,7 +428,7 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester - + - + {services.map((service) => (
- {/* Render in a portal to avoid layout/stacking shifts; prevent auto-focus to stop scroll jump */} e.preventDefault()} > diff --git a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx index 810f3eb0d07c3..ba4d114d0c4b4 100644 --- a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx @@ -269,7 +269,7 @@ export const ServiceStatus = () => { value={{overallStatusLabel}} /> - + {services.map((service) => ( } /> - +

Filter projects by status

diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx index 3d25f88e61e05..41777b56e3ca2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/SqlFunctionSection.tsx @@ -21,7 +21,6 @@ export const SqlFunctionSection = ({ form }: SqlFunctionSectionProps) => { render={({ field }) => ( diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx index 8b8939875da3d..ef6c0330bd1bb 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx @@ -328,7 +328,6 @@ const ProjectLinker = ({ side="bottom" align="center" sameWidthAsTrigger - portal > diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx index 245b0f3ceec8b..5e29dc38cdc84 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -154,7 +154,7 @@ export const BillingCustomerDataForm = ({ - + @@ -260,7 +260,7 @@ export const BillingCustomerDataForm = ({ - + diff --git a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx index 108d516567bae..6cf53e61e0784 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx @@ -92,7 +92,7 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo

- +
{config.channelName.length === 0 ? ( <> diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx index 1a355c6251ec7..2453a2d05646d 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx @@ -2,6 +2,9 @@ import { PlusCircle } from 'lucide-react' import Link from 'next/link' import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import { InlineLink } from '@/components/ui/InlineLink' +import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useParams } from 'common' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -35,9 +38,18 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter const [tempConfig, setTempConfig] = useState(config) const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() + const { data: publications } = useDatabasePublicationsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const realtimePublication = (publications ?? []).find( + (publication) => publication.name === 'supabase_realtime' + ) + // Update tempConfig when config changes to ensure consistency useEffect(() => { setTempConfig(config) @@ -75,7 +87,7 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter )} - +
Listen to event types
@@ -155,10 +167,19 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter />

- {config.enableDbChanges - ? 'Listen for Database inserts, updates, deletes and more' - : 'Enable realtime publications to listen for database changes'} + Listen for Database inserts, updates, deletes and more

+ {!config.enableDbChanges && ( +

+ Enable{' '} + + realtime publications + {' '} + for your tables to listen for database changes +

+ )}
{tempConfig.enableDbChanges && config.enableDbChanges && ( diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index 098da67887cb7..287024092ccf9 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -1,15 +1,15 @@ import { useParams } from 'common' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { EmptyRealtime } from './EmptyRealtime' import { Header } from './Header' import MessagesTable from './MessagesTable' import { SendMessageModal } from './SendMessageModal' import { RealtimeConfig, useRealtimeMessages } from './useRealtimeMessages' -import { EmptyRealtime } from './EmptyRealtime' /** * Acts as a container component for the entire log display diff --git a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx index babafd843c7ef..80cde9d7d654b 100644 --- a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx +++ b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx @@ -289,11 +289,7 @@ const ReportFilterBar = ({ Add filter - 0 ? 'end' : 'start'} - portal={true} - className="p-0 w-60" - > + 0 ? 'end' : 'start'} className="p-0 w-60">
{ setSelectedSchema(name) }} onSelectCreateSchema={() => snap.onAddSchema()} - portal={!isMobile} />
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantChatSelector.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantChatSelector.tsx index f379f6c1254a2..4dac2ec53d5f8 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistantChatSelector.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistantChatSelector.tsx @@ -98,7 +98,7 @@ export const AIAssistantChatSelector = ({ disabled = false }: AIAssistantChatSel - + No chats found. diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.Actions.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.Actions.tsx index 2170e2afee825..7ee1705bbf799 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.Actions.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Message.Actions.tsx @@ -1,22 +1,22 @@ -import { Pencil, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react' -import { type PropsWithChildren, useState, useEffect } from 'react' import { zodResolver } from '@hookform/resolvers/zod' +import { Pencil, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react' +import { type PropsWithChildren, useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import * as z from 'zod' -import { ButtonTooltip } from '../ButtonTooltip' import { - cn, Button, - Popover_Shadcn_, - PopoverTrigger_Shadcn_, - PopoverContent_Shadcn_, + cn, Form_Shadcn_, - FormField_Shadcn_, FormControl_Shadcn_, + FormField_Shadcn_, + Popover_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, TextArea_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { ButtonTooltip } from '../ButtonTooltip' export function MessageActions({ children, @@ -172,7 +172,7 @@ function MessageActionsThumbsDown({ /> - + {form.formState.isSubmitSuccessful ? (

We appreciate your feedback!

) : ( diff --git a/apps/studio/components/ui/DataTable/DataTableViewOptions.tsx b/apps/studio/components/ui/DataTable/DataTableViewOptions.tsx index 3c04c19a30a2b..668532eb6f4b3 100644 --- a/apps/studio/components/ui/DataTable/DataTableViewOptions.tsx +++ b/apps/studio/components/ui/DataTable/DataTableViewOptions.tsx @@ -46,7 +46,7 @@ export function DataTableViewOptions() { tooltip={{ content: { side: 'bottom', text: 'Toggle column visibility' } }} /> - + diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 686a9bd2a188e..2b45f0dafa7e1 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -38,8 +38,8 @@ interface DatabaseSelectorProps { additionalOptions?: { id: string; name: string }[] buttonProps?: ButtonProps onSelectId?: (id: string) => void // Optional callback - portal?: boolean className?: string + align?: 'start' | 'end' } export const DatabaseSelector = ({ @@ -48,7 +48,7 @@ export const DatabaseSelector = ({ additionalOptions = [], onSelectId = noop, buttonProps, - portal = true, + align = 'end', className, }: DatabaseSelectorProps) => { const router = useRouter() @@ -118,7 +118,7 @@ export const DatabaseSelector = ({
- + {additionalOptions.length > 0 && ( diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx index 317b3d1b60702..a052ed2dd0c94 100644 --- a/apps/studio/components/ui/FilterPopover.tsx +++ b/apps/studio/components/ui/FilterPopover.tsx @@ -208,7 +208,6 @@ export const FilterPopover = >({
diff --git a/apps/studio/components/ui/OrganizationProjectSelector.tsx b/apps/studio/components/ui/OrganizationProjectSelector.tsx index 83481a26a5b93..8105ceb42f0b5 100644 --- a/apps/studio/components/ui/OrganizationProjectSelector.tsx +++ b/apps/studio/components/ui/OrganizationProjectSelector.tsx @@ -150,7 +150,6 @@ export const OrganizationProjectSelector = ({ )} void onSelectCreateSchema?: () => void - portal?: boolean align?: 'start' | 'end' } @@ -48,7 +47,6 @@ export const SchemaSelector = ({ excludedSchemas = [], onSelectSchema, onSelectCreateSchema, - portal = true, align = 'start', }: SchemaSelectorProps) => { const [open, setOpen] = useState(false) @@ -133,11 +131,10 @@ export const SchemaSelector = ({ className="p-0 min-w-[200px] pointer-events-auto" side="bottom" align={align} - portal={portal} sameWidthAsTrigger > - + No schemas found diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index a35ed8a338715..13c60a25116e3 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -226,6 +226,8 @@ const Wizard: NextPageWithLayout = () => { { enabled: currentOrg !== null } ) + const shouldShowFreeProjectInfo = !!currentOrg && !isFreePlan + const { mutate: createProject, isPending: isCreatingNewProject, @@ -442,6 +444,23 @@ const Wizard: NextPageWithLayout = () => { {showAdvancedConfig && !!availableOrioleVersion && ( )} + + {shouldShowFreeProjectInfo ? ( + + Supabase billing is organization-based. Get 2 free projects by{' '} + + creating a free organization + + . +

+ } + /> + ) : null} )} diff --git a/apps/studio/pages/project/[ref]/functions/new.tsx b/apps/studio/pages/project/[ref]/functions/new.tsx index df447398a867c..027e33b597d20 100644 --- a/apps/studio/pages/project/[ref]/functions/new.tsx +++ b/apps/studio/pages/project/[ref]/functions/new.tsx @@ -290,7 +290,7 @@ const NewFunctionPage = () => { Templates - + diff --git a/apps/studio/pages/project/[ref]/observability/realtime.tsx b/apps/studio/pages/project/[ref]/observability/realtime.tsx index 8839a35b53c72..d183d09358c25 100644 --- a/apps/studio/pages/project/[ref]/observability/realtime.tsx +++ b/apps/studio/pages/project/[ref]/observability/realtime.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import dayjs from 'dayjs' import { ArrowRight, RefreshCw } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' @@ -23,6 +23,7 @@ import { SharedAPIReport } from 'components/interfaces/Reports/SharedAPIReport/S import { useSharedAPIReport } from 'components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants' import { realtimeReports } from 'data/reports/v2/realtime.config' import type { NextPageWithLayout } from 'types' +import { Admonition } from 'ui-patterns' import { ObservabilityLink } from 'components/ui/ObservabilityLink' const RealtimeReport: NextPageWithLayout = () => { @@ -45,6 +46,7 @@ export default RealtimeReport const RealtimeUsage = () => { const [isRefreshing, setIsRefreshing] = useState(false) const { db, chart, ref } = useParams() + const showEUAlert = useFlag('realtimeReportEUAlert') const { selectedDateRange, @@ -132,6 +134,13 @@ const RealtimeUsage = () => { return ( <> + {showEUAlert ? ( + + ) : null} diff --git a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx index 265c00f8c9cdd..89ede8d736f51 100644 --- a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx @@ -227,7 +227,6 @@ export function FilterCondition({ className="min-w-[220px] p-0" align="start" side="bottom" - portal onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} onInteractOutside={(e) => { @@ -269,7 +268,6 @@ export function FilterCondition({ className="min-w-[220px] w-fit p-0" align="start" side="bottom" - portal onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} onInteractOutside={(e) => { diff --git a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx index 660ca9ebf2486..7ef2bbf5fd006 100644 --- a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx @@ -201,7 +201,6 @@ export function FilterGroup({ group, path }: FilterGroupProps) { className="min-w-[220px] p-0" align="start" side="bottom" - portal onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} onInteractOutside={(e) => { diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx index cc16454ac9ed6..7c1111c2b78aa 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx @@ -77,7 +77,7 @@ export const ClientSelectDropdown = ({
- + diff --git a/packages/ui/src/components/shadcn/ui/popover.tsx b/packages/ui/src/components/shadcn/ui/popover.tsx index 88bf5f28a7a81..fb5f859eb0d41 100644 --- a/packages/ui/src/components/shadcn/ui/popover.tsx +++ b/packages/ui/src/components/shadcn/ui/popover.tsx @@ -11,7 +11,6 @@ const PopoverTrigger = PopoverPrimitive.Trigger const PopoverAnchor = PopoverPrimitive.Anchor type PopoverContentProps = { - portal?: boolean align?: 'center' | 'start' | 'end' sideOffset?: number sameWidthAsTrigger?: boolean @@ -20,36 +19,23 @@ type PopoverContentProps = { const PopoverContent = React.forwardRef< React.ElementRef, PopoverContentProps ->( - ( - { - className, - align = 'center', - sideOffset = 4, - portal = false, - sameWidthAsTrigger = false, - ...props - }, - ref - ) => { - const Portal = portal ? PopoverPrimitive.Portal : React.Fragment - return ( - - - - ) - } -) +>(({ className, align = 'center', sideOffset = 4, sameWidthAsTrigger = false, ...props }, ref) => { + return ( + + + + ) +}) PopoverContent.displayName = 'PopoverContent' const PopoverSeparator = React.forwardRef>( @@ -59,4 +45,4 @@ const PopoverSeparator = React.forwardRef