Merge branch 'master' into chore/data-api-integration
No files matched your search
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pnpm install
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: e2e-studio-tests
|
||||
description: Run e2e tests in the Studio app. Use when asked to run e2e tests, run studio tests, playwright tests, or test the feature.
|
||||
---
|
||||
|
||||
# E2E Studio Tests
|
||||
|
||||
Run Playwright end-to-end tests for the Studio application.
|
||||
|
||||
## Running Tests
|
||||
|
||||
Tests must be run from the `e2e/studio` directory:
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm run e2e
|
||||
```
|
||||
|
||||
### Run specific file
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
|
||||
```
|
||||
|
||||
### Run with grep filter
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
|
||||
```
|
||||
|
||||
### UI mode for debugging
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm run e2e -- --ui
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
- Tests auto-start Supabase local containers via web server config
|
||||
- Self-hosted mode (`IS_PLATFORM=false`) runs tests in parallel (3 workers)
|
||||
- No manual setup needed for self-hosted tests
|
||||
|
||||
## Test File Structure
|
||||
|
||||
- Tests are in `e2e/studio/features/*.spec.ts`
|
||||
- Use custom test utility: `import { test } from '../utils/test.js'`
|
||||
- Test fixtures provide `page`, `ref`, and other helpers
|
||||
|
||||
## Common Patterns
|
||||
|
||||
Wait for elements with generous timeouts:
|
||||
|
||||
```typescript
|
||||
await expect(locator).toBeVisible({ timeout: 30000 })
|
||||
```
|
||||
|
||||
Add messages to expects for debugging:
|
||||
|
||||
```typescript
|
||||
await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')
|
||||
```
|
||||
|
||||
Use serial mode for tests sharing database state:
|
||||
|
||||
```typescript
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
```
|
||||
|
||||
## Writing Robust Selectors
|
||||
|
||||
### Selector priority (best to worst)
|
||||
|
||||
1. **`getByRole` with accessible name** - Most robust, tests accessibility
|
||||
```typescript
|
||||
page.getByRole('button', { name: 'Save' })
|
||||
page.getByRole('button', { name: 'Configure API privileges' })
|
||||
```
|
||||
|
||||
2. **`getByTestId`** - Stable, explicit test hooks
|
||||
```typescript
|
||||
page.getByTestId('table-editor-side-panel')
|
||||
```
|
||||
|
||||
3. **`getByText` with exact match** - Good for unique text
|
||||
```typescript
|
||||
page.getByText('Data API Access', { exact: true })
|
||||
```
|
||||
|
||||
4. **`locator` with CSS** - Use sparingly, more fragile
|
||||
```typescript
|
||||
page.locator('[data-state="open"]')
|
||||
```
|
||||
|
||||
### Patterns to avoid
|
||||
|
||||
- **XPath selectors** - Fragile to DOM changes
|
||||
```typescript
|
||||
// BAD
|
||||
locator('xpath=ancestor::div[contains(@class, "space-y")]')
|
||||
```
|
||||
|
||||
- **Parent traversal with `locator('..')`** - Breaks when structure changes
|
||||
```typescript
|
||||
// BAD
|
||||
element.locator('..').getByRole('button')
|
||||
```
|
||||
|
||||
- **Broad `filter({ hasText })` on generic elements** - May match multiple elements
|
||||
```typescript
|
||||
// BAD - popover may have more than one combobox
|
||||
// Could consider scoping down the container or filtering the combobox more specifically
|
||||
popover.getByRole('combobox')
|
||||
```
|
||||
|
||||
### Add accessible labels to components
|
||||
|
||||
When a component lacks a good accessible name, add one in the source code:
|
||||
|
||||
```tsx
|
||||
// In the React component
|
||||
<Button aria-label="Configure API privileges">
|
||||
<Settings />
|
||||
</Button>
|
||||
```
|
||||
|
||||
Then use it in tests:
|
||||
```typescript
|
||||
page.getByRole('button', { name: 'Configure API privileges' })
|
||||
```
|
||||
|
||||
### Narrowing search scope
|
||||
|
||||
Scope selectors to specific containers to avoid matching wrong elements:
|
||||
|
||||
```typescript
|
||||
// Good - scoped to side panel
|
||||
const sidePanel = page.getByTestId('table-editor-side-panel')
|
||||
const toggle = sidePanel.getByRole('switch')
|
||||
|
||||
// Good - find unique element, then scope from there
|
||||
const popover = page.locator('[data-radix-popper-content-wrapper]')
|
||||
const roleSection = popover.getByText('Anonymous (anon)', { exact: true })
|
||||
```
|
||||
|
||||
## Avoiding `waitForTimeout`
|
||||
|
||||
Never use `waitForTimeout` - always wait for something specific:
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// GOOD - wait for UI element
|
||||
await expect(page.getByText('Success')).toBeVisible()
|
||||
|
||||
// GOOD - wait for API response
|
||||
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
|
||||
await saveButton.click()
|
||||
await apiPromise
|
||||
|
||||
// GOOD - wait for toast indicating operation complete
|
||||
await expect(page.getByText('Table created successfully')).toBeVisible({ timeout: 15000 })
|
||||
```
|
||||
|
||||
## Avoiding `force: true` on clicks
|
||||
|
||||
Instead of forcing clicks on hidden elements, make them visible first:
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
await menuButton.click({ force: true })
|
||||
|
||||
// GOOD - hover to reveal, then click
|
||||
await tableRow.hover()
|
||||
await expect(menuButton).toBeVisible()
|
||||
await menuButton.click()
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### View trace
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>
|
||||
```
|
||||
|
||||
### View HTML report
|
||||
|
||||
```bash
|
||||
cd e2e/studio && pnpm exec playwright show-report
|
||||
```
|
||||
|
||||
### Error context
|
||||
|
||||
Error context files are saved in the `test-results/` directory.
|
||||
|
||||
### Playwright MCP tools
|
||||
|
||||
Use Playwright MCP tools to inspect UI when debugging locally.
|
||||
|
||||
## CI vs Local Development
|
||||
|
||||
The key difference is **cold start vs warm state**:
|
||||
|
||||
### CI (cold start)
|
||||
|
||||
Tests run from a blank database slate. Each test run resets the database and starts fresh containers. Extensions like pg_cron are NOT enabled by default.
|
||||
|
||||
### Local dev with `pnpm dev:studio-local`
|
||||
|
||||
When debugging with a running dev server, the database may already have state from previous runs (extensions enabled, test data present).
|
||||
|
||||
## Handling Cold Start Bugs
|
||||
|
||||
Tests that work locally but fail in CI often have assumptions about existing state.
|
||||
|
||||
### Common issues
|
||||
|
||||
1. Extension not enabled (must enable in test setup)
|
||||
2. Race conditions when parallel tests try to modify shared state (use `test.describe.configure({ mode: 'serial' })`)
|
||||
3. Locators matching wrong elements because the page structure differs when state isn't set up
|
||||
|
||||
### Reproducing CI behavior locally
|
||||
|
||||
The test framework automatically resets the database when running `pnpm run e2e`. This matches CI behavior.
|
||||
|
||||
If using `pnpm dev:studio-local` for Playwright MCP debugging, remember the state differs from CI.
|
||||
|
||||
## Debugging Workflow for CI Failures
|
||||
|
||||
1. First, run the test locally with `pnpm run e2e -- features/<file>.spec.ts` (cold start)
|
||||
2. Check error context in `test-results/` directory
|
||||
3. If you need to inspect UI state, start `pnpm dev:studio-local` and use Playwright MCP tools
|
||||
4. Remember: what you see in the dev server may have state that doesn't exist in CI
|
||||
@@ -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<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// 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<NewQueryModel[] | GraphQLError> {
|
||||
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<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
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 <queryName>.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.
|
||||
@@ -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.
|
||||
|
||||
<rule>
|
||||
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
|
||||
</rule>
|
||||
@@ -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
|
||||
|
||||
@@ -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<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
<ScaffoldContainer>
|
||||
<MyPageComponent />
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
MyPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>{page}</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default MyPage
|
||||
|
||||
export const MyPageComponent = () => (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div>
|
||||
<ScaffoldSectionTitle>My page section</ScaffoldSectionTitle>
|
||||
<ScaffoldSectionDescription>A brief description of the purpose of the page</ScaffoldSectionDescription>
|
||||
</div>
|
||||
// Content goes here
|
||||
</ScaffoldSection>
|
||||
)
|
||||
```
|
||||
|
||||
## 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<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { name: '', enableFeature: false },
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// handle mutation with onSuccess/onError toast
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Name"
|
||||
description="A descriptive name for this resource"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} placeholder="Enter name" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="enableFeature"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Enable Feature"
|
||||
description="Toggle this feature on or off"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button type="default" onClick={() => form.reset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { name: '' },
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// handle mutation
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent size="lg" className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create Resource</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="overflow-auto flex-grow px-0"
|
||||
>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label="Name" description="A descriptive name">
|
||||
<FormControl_Shadcn_ className="col-span-6 min-w-100">
|
||||
<Input_Shadcn_ {...field} placeholder="Enter name" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<SheetFooter>
|
||||
<Button type="default" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" form={formId} htmlType="submit">
|
||||
Create
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: `<PrePostTab postTab="MB"><Input_Shadcn_ /></PrePostTab>`
|
||||
- **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'
|
||||
;<Table>
|
||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Invoice</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">INV001</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
```
|
||||
|
||||
## 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
|
||||
<Admonition
|
||||
type="note"
|
||||
title="No authentication logs available for this user"
|
||||
description="Auth events such as logging in will be shown here"
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
description: Guidelines for using the useStaticEffectEvent hook in Studio - a polyfill for React's useEffectEvent pattern
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# useStaticEffectEvent Hook
|
||||
|
||||
The `useStaticEffectEvent` hook (located at `apps/studio/hooks/useStaticEffectEvent.ts`) is a userland implementation of React's `useEffectEvent` pattern. It solves the stale closure problem by providing a stable callback reference that always accesses the latest props and state values.
|
||||
|
||||
## What Problem Does It Solve?
|
||||
|
||||
When using `useEffect`, you often need to access props or state inside your Effect, but you don't want changes to those values to re-run the Effect. Without `useStaticEffectEvent`, you'd face two bad options:
|
||||
|
||||
1. **Add them to dependencies** - causes unnecessary Effect re-runs (teardown/reconnect cycles)
|
||||
2. **Omit from dependencies** - causes stale closure bugs where your callback uses outdated values
|
||||
|
||||
```tsx
|
||||
// Problem: This Effect re-runs every time `theme` changes, even though
|
||||
// we only want to reconnect when `roomId` changes
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.on('connected', () => {
|
||||
showNotification('Connected!', theme) // `theme` causes unwanted re-runs
|
||||
})
|
||||
return () => connection.disconnect()
|
||||
}, [roomId, theme]) // Adding theme causes unnecessary reconnections
|
||||
```
|
||||
|
||||
## When to Use useStaticEffectEvent
|
||||
|
||||
Use `useStaticEffectEvent` when you need to:
|
||||
|
||||
1. **Read latest state/props inside an Effect without re-triggering it**
|
||||
2. **Create stable callbacks that always use current values**
|
||||
3. **Avoid stale closure bugs in event handlers used within Effects**
|
||||
|
||||
### Pattern 1: Syncing data without re-running on every change
|
||||
|
||||
```tsx
|
||||
// ✅ Good - sync data when status changes, but always read latest state
|
||||
const syncApiPrivileges = useStaticEffectEvent(() => {
|
||||
if (hasLoadedInitialData.current) return
|
||||
if (!apiAccessStatus.isSuccess) return
|
||||
if (!privilegesForTable) return
|
||||
|
||||
hasLoadedInitialData.current = true
|
||||
setPrivileges(privilegesForTable.privileges)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
syncApiPrivileges()
|
||||
}, [apiAccessStatus.status, syncApiPrivileges])
|
||||
```
|
||||
|
||||
### Pattern 2: Stable callbacks for async operations
|
||||
|
||||
```tsx
|
||||
// ✅ Good - wrap complex async logic that reads many values
|
||||
const exportInternal = useStaticEffectEvent(
|
||||
async ({ bypassConfirmation }: { bypassConfirmation: boolean }): Promise<void> => {
|
||||
if (!params.enabled) return
|
||||
const { projectRef, connectionString, entity, totalRows } = params
|
||||
// ... complex async logic using latest params
|
||||
}
|
||||
)
|
||||
|
||||
// This callback is stable and can be safely used in useCallback
|
||||
const exportInDesiredFormat = useCallback(
|
||||
() => exportInternal({ bypassConfirmation: false }),
|
||||
[exportInternal]
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Infinite scroll / pagination triggers
|
||||
|
||||
```tsx
|
||||
// ✅ Good - always read latest pagination state when scrolling triggers fetch
|
||||
const fetchNext = useStaticEffectEvent(() => {
|
||||
if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(fetchNext, [lastItem, fetchNext])
|
||||
```
|
||||
|
||||
## When NOT to Use useStaticEffectEvent
|
||||
|
||||
### Don't use it to avoid specifying legitimate dependencies
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - hiding the fact that this should re-run when roomId changes
|
||||
const connect = useStaticEffectEvent(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.connect()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
connect() // BUG: Won't reconnect when roomId changes!
|
||||
}, [connect])
|
||||
|
||||
// ✅ Good - roomId is a legitimate dependency
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.connect()
|
||||
return () => connection.disconnect()
|
||||
}, [roomId])
|
||||
```
|
||||
|
||||
### Don't use it for simple event handlers outside Effects
|
||||
|
||||
```tsx
|
||||
// ❌ Unnecessary - not used inside an Effect
|
||||
const handleClick = useStaticEffectEvent(() => {
|
||||
console.log(count)
|
||||
})
|
||||
|
||||
// ✅ Good - regular function or useCallback is fine
|
||||
const handleClick = () => {
|
||||
console.log(count)
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The hook uses a ref to store the latest callback and returns a stable wrapper function:
|
||||
|
||||
```tsx
|
||||
export const useStaticEffectEvent = <Callback extends Function>(callback: Callback) => {
|
||||
const callbackRef = useRef(callback)
|
||||
|
||||
// Update the ref on every render with the latest callback
|
||||
useLayoutEffect(() => {
|
||||
callbackRef.current = callback
|
||||
})
|
||||
|
||||
// Return a stable function that calls the latest callback
|
||||
const eventFn = useCallback((...args: any) => {
|
||||
return callbackRef.current(...args)
|
||||
}, [])
|
||||
|
||||
return eventFn as unknown as Callback
|
||||
}
|
||||
```
|
||||
|
||||
## Relationship to React's useEffectEvent
|
||||
|
||||
This hook is a polyfill for React's experimental `useEffectEvent` (now stable in React 19.2). The core concept is identical:
|
||||
|
||||
- Extract non-reactive logic into a stable function
|
||||
- Always access the latest props/state without adding them as Effect dependencies
|
||||
- Should only be called from within Effects
|
||||
|
||||
When React's `useEffectEvent` becomes widely available, this hook can be replaced with the official API.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Only call the returned function inside Effects** (useEffect, useLayoutEffect)
|
||||
2. **Don't pass the function to other components or hooks** as a callback prop
|
||||
3. **Use for non-reactive logic only** - logic that reads values but shouldn't trigger re-runs
|
||||
4. **Include it in dependency arrays** when used in useEffect (the function is stable, so it won't cause re-runs)
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
<ExpensiveList
|
||||
items={items}
|
||||
onItemClick={(item) => handleItemClick(item)}
|
||||
/>
|
||||
<ExpensiveList items={items} onItemClick={(item) => handleItemClick(item)} />
|
||||
|
||||
// ✅ Good - stable reference with useCallback
|
||||
const handleItemClick = useCallback((item: Item) => {
|
||||
// handle click
|
||||
}, [dependencies])
|
||||
const handleItemClick = useCallback(
|
||||
(item: Item) => {
|
||||
// handle click
|
||||
},
|
||||
[dependencies]
|
||||
)
|
||||
|
||||
<ExpensiveList items={items} onItemClick={handleItemClick} />
|
||||
```
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 `<form>`, set a stable `formId` and use the button’s `form` prop.
|
||||
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<ReturnType<typeof getX>>
|
||||
|
||||
export const useXQuery = <TData = XData>(
|
||||
{ projectRef }: XVariables,
|
||||
{ enabled = true, ...options }: UseCustomQueryOptions<XData, XError, TData> = {}
|
||||
) =>
|
||||
useQuery<XData, XError, TData>({
|
||||
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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Authorize Vercel Deploys
|
||||
|
||||
# This workflow is triggered by the validate-pr workflow. When it's triggered, it will run the
|
||||
# authorize-vercel-deploys.yml in master branch. If you want to change it, you'll have to merge it into master.
|
||||
on:
|
||||
# only run this workflow when the validate-pr workflow completes (successfully or not)
|
||||
workflow_run:
|
||||
workflows: ['Validate pull request']
|
||||
types: [completed]
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
authorize-vercel-deploys:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
# Checkout the master branch from the supabase repo and run that script to authorize Vercel deploys
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
ref: master
|
||||
# fetch only the root files and scripts folder
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Authorize Vercel Deploys
|
||||
run: |-
|
||||
pnpm run authorize-vercel-deploys
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
# The SHA of the commit that triggered the validate-pr workflow
|
||||
HEAD_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
@@ -54,14 +54,21 @@ jobs:
|
||||
echo "Generating new typespec snapshot for review..."
|
||||
npx vitest run --update ./features/docs/Reference.typeSpec.test.ts
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'docs: update js client libraries (${{ github.event.inputs.version }})'
|
||||
title: 'docs: update js client libraries (${{ github.event.inputs.version }})'
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
|
||||
title: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
|
||||
body: |
|
||||
Updates JS client libraries documentation following stable release.
|
||||
Updates JS sdk documentation following stable release.
|
||||
Ran `make` in apps/docs/spec to regenerate tsdoc files.
|
||||
|
||||
**Details:**
|
||||
@@ -69,6 +76,6 @@ jobs:
|
||||
- **Source:** `${{ github.event.inputs.source }}`
|
||||
- **Changes:** Regenerated tsdoc files from latest spec files
|
||||
|
||||
🤖 Auto-generated from supabase-js-libs stable release.
|
||||
branch: 'gha/update-js-libs-docs-${{ github.run_number }}'
|
||||
🤖 Auto-generated from @supabase/supabase-js stable release.
|
||||
branch: 'gha/update-js-sdk-docs-${{ github.run_number }}'
|
||||
base: 'master'
|
||||
@@ -39,10 +39,17 @@ jobs:
|
||||
working-directory: apps/docs/spec
|
||||
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'feat: update mgmt api docs'
|
||||
title: 'feat: update mgmt api docs'
|
||||
body: 'This PR updates mgmt api docs automatically.'
|
||||
|
||||
@@ -54,3 +54,4 @@ jobs:
|
||||
author: 'github-docs-sync-bot <github-docs-sync-bot@supabase.com>'
|
||||
branch: 'bot/docs-sync-troubleshooting'
|
||||
branch-suffix: 'random'
|
||||
labels: 'documentation'
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
@@ -35,9 +35,16 @@ jobs:
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Decrease ESLint ratchet baselines and open PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
@@ -56,11 +56,18 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
|
||||
title: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
|
||||
body: |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Validate pull request
|
||||
|
||||
# This workflow will trigger the authorize-vercel-deploys workflow when it's finished.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, synchronize, ready_for_review]
|
||||
|
||||
@@ -117,7 +117,11 @@ next-env.d.ts
|
||||
.vercel
|
||||
|
||||
# AI assistant local files
|
||||
.claude/
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/scripts/
|
||||
!.claude/skills/
|
||||
.claude/skills/me-*
|
||||
CLAUDE.md
|
||||
|
||||
#include template .env file for docker-compose
|
||||
@@ -149,4 +153,4 @@ gcloud.json
|
||||
# Sentry CLI config
|
||||
**/.sentryclirc
|
||||
|
||||
keys.json
|
||||
keys.json
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf",
|
||||
"sqlKeywordCase": "lower",
|
||||
"plugins": ["prettier-plugin-sql-cst"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.json",
|
||||
"options": { "parser": "json" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -35,7 +35,7 @@ To ensure a positive and inclusive environment, please read our [code of conduct
|
||||
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
|
||||
|
||||
- [Git](https://git-scm.com/)
|
||||
- [Node.js v22.x or higher](https://nodejs.org)
|
||||
- [Node.js](https://nodejs.org) version as documented in [.nvmrc](./.nvmrc)
|
||||
- [pnpm](https://pnpm.io/) version 9.x.x or higher
|
||||
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
|
||||
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
|
||||
|
||||
@@ -60,9 +60,9 @@ With that out of the way, there are several parts of this design system that nee
|
||||
- `config/docs.ts`: list of components in the sidebar
|
||||
- `content/docs`: the actual component documentation
|
||||
- `registry/examples.ts`: list of example components
|
||||
- `registry/default/example`: the actual example components
|
||||
- `registry/charts.ts`: chart components
|
||||
- `registry/fragments.ts`: fragment components
|
||||
- `registry/fragments.ts`: list of fragment components
|
||||
- `registry/charts.ts`: list of chart components
|
||||
- `registry/default/example/*`: the actual example components
|
||||
|
||||
You will probably need to rebuild the design system’s registry after making new additions. You can do that via:
|
||||
|
||||
|
||||
@@ -28,16 +28,38 @@ export default function ComposedChartBasic() {
|
||||
},
|
||||
]
|
||||
|
||||
const data = Array.from({ length: 46 }, (_, i) => {
|
||||
const data = Array.from({ length: 40 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
|
||||
date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart
|
||||
|
||||
const progress = i / 40
|
||||
const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12)
|
||||
const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10)
|
||||
const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12)
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
standard_score: Math.floor(Math.random() * 100),
|
||||
standard_score: Math.max(0, Math.min(100, standard_score)),
|
||||
performance: Math.max(0, Math.min(100, performance)),
|
||||
efficiency: Math.max(0, Math.min(100, efficiency)),
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
const chartConfig = {
|
||||
standard_score: {
|
||||
label: 'Standard Score',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
},
|
||||
performance: {
|
||||
label: 'Performance',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
efficiency: {
|
||||
label: 'Efficiency',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
@@ -50,10 +72,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
|
||||
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
@@ -86,10 +106,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Line Chart</ChartTitle>
|
||||
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
@@ -105,6 +123,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartLine
|
||||
data={data}
|
||||
dataKey="standard_score"
|
||||
dataKeys={['standard_score', 'performance', 'efficiency']}
|
||||
config={chartConfig}
|
||||
showGrid={true}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
|
||||
@@ -159,17 +159,6 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"aspect-ratio-demo": {
|
||||
name: "aspect-ratio-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["aspect-ratio"],
|
||||
component: React.lazy(() => import("@/registry/default/example/aspect-ratio-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/aspect-ratio-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"alert-dialog-destructive": {
|
||||
name: "alert-dialog-destructive",
|
||||
type: "components:example",
|
||||
@@ -192,6 +181,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"aspect-ratio-demo": {
|
||||
name: "aspect-ratio-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["aspect-ratio"],
|
||||
component: React.lazy(() => import("@/registry/default/example/aspect-ratio-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/aspect-ratio-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"avatar-demo": {
|
||||
name: "avatar-demo",
|
||||
type: "components:example",
|
||||
@@ -2238,6 +2238,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"page-section-title-only": {
|
||||
name: "page-section-title-only",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/page-section-title-only")),
|
||||
source: "",
|
||||
files: ["registry/default/example/page-section-title-only.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"page-section-with-aside": {
|
||||
name: "page-section-with-aside",
|
||||
type: "components:example",
|
||||
|
||||
@@ -1,15 +1,118 @@
|
||||
import '@/styles/globals.css'
|
||||
import '../../studio/styles/typography.scss'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
|
||||
import { ThemeProvider } from './Providers'
|
||||
import { SonnerToaster } from './SonnerToast'
|
||||
import { customFont, sourceCodePro } from './fonts'
|
||||
|
||||
const className = `${customFont.variable} ${sourceCodePro.variable}`
|
||||
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
|
||||
|
||||
const genFaviconData = (basePath: string): Metadata['icons'] => ({
|
||||
icon: {
|
||||
url: `${basePath}/favicon/favicon.ico`,
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
shortcut: `${basePath}/favicon/favicon.ico`,
|
||||
apple: `${basePath}/favicon/favicon.ico`,
|
||||
other: [
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-57x57.png`,
|
||||
sizes: '57x57',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-60x60.png`,
|
||||
sizes: '60x60',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-72x72.png`,
|
||||
sizes: '72x72',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-76x76.png`,
|
||||
sizes: '76x76',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-114x114.png`,
|
||||
sizes: '114x114',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-120x120.png`,
|
||||
sizes: '120x120',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-144x144.png`,
|
||||
sizes: '144x144',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: `${basePath}/favicon/apple-icon-152x152.png`,
|
||||
sizes: '152x152',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-16x16.png`,
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-32x32.png`,
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-48x48.png`,
|
||||
type: 'image/png',
|
||||
sizes: '48x48',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-96x96.png`,
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-128x128.png`,
|
||||
type: 'image/png',
|
||||
sizes: '128x128',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-180x180.png`,
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
url: `${basePath}/favicon/favicon-196x196.png`,
|
||||
type: 'image/png',
|
||||
sizes: '196x196',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
applicationName: 'Supabase Design System',
|
||||
title: 'Supabase Design System',
|
||||
description: 'Design resources for building consistent user experiences at Supabase.',
|
||||
icons: genFaviconData(BASE_PATH),
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: '#1E1E1E',
|
||||
}
|
||||
|
||||
interface RootLayoutProps {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const docsConfig: DocsConfig = {
|
||||
items: [
|
||||
{
|
||||
title: 'Introduction',
|
||||
href: '/docs/ui-patterns/ui-patterns',
|
||||
href: '/docs/ui-patterns/introduction',
|
||||
items: [],
|
||||
priority: true,
|
||||
},
|
||||
@@ -108,7 +108,7 @@ export const docsConfig: DocsConfig = {
|
||||
items: [
|
||||
{
|
||||
title: 'Introduction',
|
||||
href: '/docs/fragments/fragment-components',
|
||||
href: '/docs/fragments/introduction',
|
||||
items: [],
|
||||
priority: true,
|
||||
},
|
||||
@@ -210,7 +210,7 @@ export const docsConfig: DocsConfig = {
|
||||
items: [
|
||||
{
|
||||
title: 'Introduction',
|
||||
href: '/docs/components/atom-components',
|
||||
href: '/docs/components/introduction',
|
||||
items: [],
|
||||
priority: true,
|
||||
},
|
||||
|
||||
@@ -199,11 +199,13 @@ Avoid adding other actions when using row-level navigation, as multiple interact
|
||||
|
||||
<ComponentPreview name="table-row-link" />
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -85,130 +85,44 @@ interface FilterCondition {
|
||||
|
||||
### Component Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| -------------------- | ------------------------------ | ---------------------------------------- |
|
||||
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
|
||||
| filters | FilterGroup | Current filter state |
|
||||
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
|
||||
| freeformText | string | Current free-form search text |
|
||||
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
|
||||
| aiApiUrl | string? | Optional URL for AI-powered filtering |
|
||||
| Prop | Type | Description |
|
||||
| -------------------- | ------------------------------ | ----------------------------------------------- |
|
||||
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
|
||||
| filters | FilterGroup | Current filter state |
|
||||
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
|
||||
| freeformText | string | Current free-form search text |
|
||||
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
|
||||
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
|
||||
| isLoading | boolean? | If true, dims the bar while work is in progress |
|
||||
|
||||
## AI Integration
|
||||
## Custom actions (e.g. AI)
|
||||
|
||||
The Filter Bar component supports AI-powered filtering through an optional API endpoint. When `aiApiUrl` is provided, the component will send natural language queries to be converted into structured filters.
|
||||
|
||||
### API Endpoint
|
||||
|
||||
The AI API endpoint should accept POST requests with the following structure:
|
||||
|
||||
```typescript
|
||||
// Request body
|
||||
interface AIFilterRequest {
|
||||
prompt: string // Natural language query
|
||||
filterProperties: FilterProperty[] // Available filter properties
|
||||
}
|
||||
|
||||
// Response body
|
||||
interface AIFilterResponse {
|
||||
logicalOperator: 'AND' | 'OR'
|
||||
conditions: (FilterCondition | FilterGroup)[]
|
||||
}
|
||||
```
|
||||
|
||||
### Example API Implementation
|
||||
|
||||
```typescript
|
||||
import { generateObject } from 'ai'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Define schemas for validation
|
||||
const FilterProperty = z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['string', 'number', 'date', 'boolean']),
|
||||
options: z.array(z.string()).optional(),
|
||||
operators: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
const FilterCondition = z.object({
|
||||
propertyName: z.string(),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
||||
operator: z.string(),
|
||||
})
|
||||
|
||||
type FilterGroupType = {
|
||||
logicalOperator: 'AND' | 'OR'
|
||||
conditions: Array<z.infer<typeof FilterCondition> | FilterGroupType>
|
||||
}
|
||||
|
||||
const FilterGroup: z.ZodType<FilterGroupType> = z.lazy(() =>
|
||||
z.object({
|
||||
logicalOperator: z.enum(['AND', 'OR']),
|
||||
conditions: z.array(z.union([FilterCondition, FilterGroup])),
|
||||
})
|
||||
)
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { prompt, filterProperties } = await req.json()
|
||||
const filterPropertiesString = JSON.stringify(filterProperties)
|
||||
|
||||
try {
|
||||
const { object } = await generateObject({
|
||||
model: openai('gpt-4-mini'),
|
||||
schema: FilterGroup,
|
||||
prompt: `Generate a filter group based on the following prompt: "${prompt}".
|
||||
Use only these filter properties: ${filterPropertiesString}.
|
||||
Each property has its own set of valid operators defined in the operators field.
|
||||
Return a filter group with a logical operator ('AND'/'OR') and an array of conditions.
|
||||
Each condition can be either a filter condition or another filter group.
|
||||
Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }.
|
||||
Ensure that the generated filters use only the provided property names and their corresponding operators.`,
|
||||
})
|
||||
|
||||
// Validate that all propertyNames exist in filterProperties
|
||||
const validatePropertyNames = (group: FilterGroupType): boolean => {
|
||||
return group.conditions.every((condition) => {
|
||||
if ('logicalOperator' in condition) {
|
||||
return validatePropertyNames(condition as FilterGroupType)
|
||||
}
|
||||
const property = filterProperties.find(
|
||||
(p: z.infer<typeof FilterProperty>) => p.name === condition.propertyName
|
||||
)
|
||||
if (!property) return false
|
||||
// Validate operator is valid for this property
|
||||
return property.operators?.includes(condition.operator) ?? false
|
||||
})
|
||||
}
|
||||
|
||||
if (!validatePropertyNames(object)) {
|
||||
throw new Error('Invalid property names or operators in generated filter')
|
||||
}
|
||||
|
||||
// Zod will throw an error if the object doesn't match the schema
|
||||
const validatedFilters = FilterGroup.parse(object)
|
||||
return Response.json(validatedFilters)
|
||||
} catch (error: any) {
|
||||
console.error('Error in AI filtering:', error)
|
||||
return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with AI
|
||||
You can append custom actions to the property menu. Each action receives the current free-form input value and the active group's path so you can plug in AI, saved queries, etc.
|
||||
|
||||
```tsx
|
||||
export function FilterDemoWithAI() {
|
||||
const [filters, setFilters] = useState<FilterGroup>(initialFilters)
|
||||
const actions = [
|
||||
{
|
||||
value: 'ai-filter',
|
||||
label: 'Filter by AI',
|
||||
onSelect: async (inputValue, { path }) => {
|
||||
const response = await fetch('/api/filter-ai', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt: inputValue, path }),
|
||||
})
|
||||
const group = (await response.json()) as FilterGroup
|
||||
// Replace your filter state at the provided path with the returned group
|
||||
setFilters((prev) => updateGroupAtPath(prev, path, group))
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
filterProperties={filterProperties}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
aiApiUrl="/api/filter-ai" // Enable AI filtering
|
||||
/>
|
||||
)
|
||||
}
|
||||
<FilterBar
|
||||
filterProperties={filterProperties}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
freeformText={freeformText}
|
||||
onFreeformTextChange={setFreeformText}
|
||||
actions={actions}
|
||||
/>
|
||||
```
|
||||
@@ -14,11 +14,11 @@ fragment: true
|
||||
## Sub-components
|
||||
|
||||
- `PageSection` - Root container with orientation variants (`horizontal` or `vertical`)
|
||||
- `PageSectionMeta` - Meta wrapper for summary and aside (groups summary and aside together)
|
||||
- `PageSectionSummary` - Container for section title and description (should be inside PageSectionMeta, has `flex-1`)
|
||||
- `PageSectionMeta` - Meta wrapper for `PageSectionSummary` and optional `PageSectionAside`
|
||||
- `PageSectionSummary` - Container for section title and description (should be inside `PageSectionMeta`, has `flex-1`)
|
||||
- `PageSectionTitle` - Section heading (h2)
|
||||
- `PageSectionDescription` - Supporting text below section title
|
||||
- `PageSectionAside` - Container for section-level actions (should be inside PageSectionMeta, has `shrink-0`)
|
||||
- `PageSectionAside` - Container for section-level actions (should be inside `PageSectionMeta`, has `shrink-0`)
|
||||
- `PageSectionContent` - Container for the main section content
|
||||
|
||||
## Orientation Variants
|
||||
@@ -45,3 +45,12 @@ fragment: true
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
### Without Aside
|
||||
|
||||
<ComponentPreview
|
||||
name="page-section-title-only"
|
||||
description="PageSection with title only"
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
@@ -7,10 +7,10 @@ UI patterns are reusable design solutions that combine multiple components from
|
||||
|
||||
These patterns help ensure consistency across Supabase products by establishing standard approaches for:
|
||||
|
||||
- **[Charts](/docs/ui-patterns/charts)**: Visualizing data consistently using standardized chart types and styling.
|
||||
- **[Empty States](/docs/ui-patterns/empty-states)**: Communicating the absence of data and guiding users toward meaningful actions.
|
||||
- **[Forms](/docs/ui-patterns/forms)**: Building cohesive form experiences in both page layouts and side panels.
|
||||
- **[Layout](/docs/ui-patterns/layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization.
|
||||
- **[Navigation](/docs/ui-patterns/navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts.
|
||||
- **[Charts](charts)**: Visualizing data consistently using standardized chart types and styling.
|
||||
- **[Empty States](empty-states)**: Communicating the absence of data and guiding users toward meaningful actions.
|
||||
- **[Forms](forms)**: Building cohesive form experiences in both page layouts and side panels.
|
||||
- **[Layout](layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization.
|
||||
- **[Navigation](navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts.
|
||||
|
||||
UI patterns may incorporate external libraries (such as `react-markdown`, `reactflow`, `recharts`, etc) or compose various components from the `ui` package. They serve as blueprints for solving recurring design problems, ensuring that similar features across the application follow the same structural and interaction patterns.
|
||||
@@ -75,6 +75,6 @@ Detail pages display dense or lengthy content split into multiple sections. The
|
||||
|
||||
## Components
|
||||
|
||||
- **[PageContainer](/docs/fragments/page-container)** - Container component providing consistent max-width and padding based on size variants
|
||||
- **[PageHeader](/docs/fragments/page-header)** - Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation
|
||||
- **[PageSection](/docs/fragments/page-section)** - Compound component for organizing page content into distinct sections with title, description, and action areas
|
||||
- **[Page Container](../fragments/page-container)**: Container component providing consistent max-width and padding based on size variants
|
||||
- **[Page Header](../fragments/page-header)**: Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation
|
||||
- **[Page Section](../fragments/page-section)**: Compound component for organizing page content into distinct sections with title, description, and action areas
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 854 B |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 1007 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/favicon/ms-icon-70x70.png"/><square150x150logo src="/favicon/ms-icon-150x150.png"/><square310x310logo src="/favicon/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 314 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 558 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Supabase Design System",
|
||||
"short_name": "Supabase Design System",
|
||||
"description": "Design resources for building consistent user experiences at Supabase.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 955 B |
@@ -28,16 +28,38 @@ export default function ComposedChartBasic() {
|
||||
},
|
||||
]
|
||||
|
||||
const data = Array.from({ length: 46 }, (_, i) => {
|
||||
const data = Array.from({ length: 40 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
|
||||
date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart
|
||||
|
||||
const progress = i / 40
|
||||
const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12)
|
||||
const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10)
|
||||
const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12)
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
standard_score: Math.floor(Math.random() * 100),
|
||||
standard_score: Math.max(0, Math.min(100, standard_score)),
|
||||
performance: Math.max(0, Math.min(100, performance)),
|
||||
efficiency: Math.max(0, Math.min(100, efficiency)),
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
const chartConfig = {
|
||||
standard_score: {
|
||||
label: 'Standard Score',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
},
|
||||
performance: {
|
||||
label: 'Performance',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
efficiency: {
|
||||
label: 'Efficiency',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
@@ -50,10 +72,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
|
||||
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
@@ -86,10 +106,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Line Chart</ChartTitle>
|
||||
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
@@ -105,6 +123,8 @@ export default function ComposedChartBasic() {
|
||||
<ChartLine
|
||||
data={data}
|
||||
dataKey="standard_score"
|
||||
dataKeys={['standard_score', 'performance', 'efficiency']}
|
||||
config={chartConfig}
|
||||
showGrid={true}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { format } from 'date-fns'
|
||||
import { useState } from 'react'
|
||||
import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui'
|
||||
import { DateRange } from 'react-day-picker'
|
||||
import { Button, Calendar } from 'ui'
|
||||
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns'
|
||||
|
||||
function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
const [date, setDate] = useState<any | undefined>(
|
||||
const [date, setDate] = useState<DateRange | undefined>(
|
||||
search
|
||||
? {
|
||||
from: new Date(search),
|
||||
@@ -46,46 +47,18 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
const [time, setTime] = useState(search || '')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Select Time</h3>
|
||||
<Input_Shadcn_ type="time" value={time} onChange={(e) => setTime(e.target.value)} />
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button_Shadcn_ variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button_Shadcn_>
|
||||
<Button_Shadcn_ onClick={() => onChange(time)}>Apply</Button_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
const [range, setRange] = useState(search || '')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Select Range</h3>
|
||||
<Input_Shadcn_ type="range" value={range} onChange={(e) => setRange(e.target.value)} />
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button_Shadcn_ variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button_Shadcn_>
|
||||
<Button_Shadcn_ onClick={() => onChange(range)}>Apply</Button_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const filterProperties = [
|
||||
{
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: 'string' as const,
|
||||
operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'],
|
||||
operators: [
|
||||
{ value: '=', label: 'Equals' },
|
||||
{ value: '!=', label: 'Not equals' },
|
||||
{ value: 'CONTAINS', label: 'Contains' },
|
||||
{ value: 'STARTS WITH', label: 'Starts with' },
|
||||
{ value: 'ENDS WITH', label: 'Ends with' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
@@ -138,7 +111,6 @@ const filterProperties = [
|
||||
</div>
|
||||
),
|
||||
},
|
||||
triggerOnPropertyClick: true,
|
||||
operators: ['=', '!=', '>', '<', '>=', '<='],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Card, CardContent } from 'ui'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionContent,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
|
||||
export default function PageSectionTitleOnly() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Section Title</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<p className="text-sm text-foreground-light">
|
||||
PageSectionSummary should still be wrapped in PageSectionMeta, as the latter is a
|
||||
flex container that will allow the former to span its full width.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1263,6 +1263,11 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/page-section-horizontal.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'page-section-title-only',
|
||||
type: 'components:example',
|
||||
files: ['example/page-section-title-only.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'page-section-with-aside',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -15,7 +15,13 @@ type FeedbackModalProps = {
|
||||
|
||||
function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps) {
|
||||
return (
|
||||
<Modal hideFooter header="Leave a comment" visible={visible} onEscapeKeyDown={onCancel}>
|
||||
<Modal
|
||||
hideFooter
|
||||
header="Leave a comment"
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onEscapeKeyDown={onCancel}
|
||||
>
|
||||
<Form
|
||||
initialValues={{ page, comment: '' }}
|
||||
validateOnBlur
|
||||
|
||||
@@ -727,6 +727,7 @@ export const auth: NavMenuConstant = {
|
||||
name: 'Server-Side Rendering',
|
||||
url: '/guides/auth/server-side',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/auth/server-side' },
|
||||
{ name: 'Creating a client', url: '/guides/auth/server-side/creating-a-client' },
|
||||
{
|
||||
name: 'Migrating from Auth Helpers',
|
||||
@@ -1481,6 +1482,11 @@ export const api: NavMenuConstant = {
|
||||
url: '/guides/api/rest/generating-types',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Generating Python Types',
|
||||
url: '/guides/api/rest/generating-python-types',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Tools',
|
||||
url: '/guides/api',
|
||||
@@ -1493,6 +1499,7 @@ export const api: NavMenuConstant = {
|
||||
{ name: 'Creating API routes', url: '/guides/api/creating-routes' },
|
||||
{ name: 'How API Keys work', url: '/guides/api/api-keys' },
|
||||
{ name: 'Securing your API', url: '/guides/api/securing-your-api' },
|
||||
{ name: 'Error Codes', url: '/guides/api/rest/postgrest-error-codes' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1663,7 +1670,14 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Integrations',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Supabase Auth', url: '/guides/functions/auth' },
|
||||
{
|
||||
name: 'Supabase Auth',
|
||||
url: '/guides/functions/auth',
|
||||
items: [
|
||||
{ name: 'Securing your functions', url: '/guides/functions/auth' },
|
||||
{ name: 'Legacy JWT secret', url: '/guides/functions/auth-legacy-jwt' },
|
||||
],
|
||||
},
|
||||
{ name: 'Supabase Database (Postgres)', url: '/guides/functions/connect-to-postgres' },
|
||||
{ name: 'Supabase Storage', url: '/guides/functions/storage-caching' },
|
||||
],
|
||||
@@ -1836,6 +1850,10 @@ export const realtime: NavMenuConstant = {
|
||||
name: 'Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Realtime Reports',
|
||||
url: '/guides/realtime/reports' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Subscribing to Database Changes',
|
||||
url: '/guides/realtime/subscribing-to-database-changes' as `/${string}`,
|
||||
@@ -1858,7 +1876,7 @@ export const realtime: NavMenuConstant = {
|
||||
name: 'Deep dive',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Quotas', url: '/guides/realtime/quotas', enabled: billingEnabled },
|
||||
{ name: 'Limits', url: '/guides/realtime/limits', enabled: billingEnabled },
|
||||
{
|
||||
name: 'Pricing',
|
||||
url: '/guides/realtime/pricing' as `/${string}`,
|
||||
|
||||
@@ -58,6 +58,7 @@ const FunctionLink = memo(function FunctionLink({
|
||||
*/
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
menuState.setMenuActiveRefId(id)
|
||||
history.pushState({}, '', url)
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
document.getElementById(slug)?.scrollIntoView({
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
This partial has {{ .my-var }}, {{ .another_var }}, and {{ .myVar123 }}.
|
||||
@@ -0,0 +1 @@
|
||||
This partial has {{ .var1 }}, {{ .var2 }}, and {{ .var3 }}.
|
||||
@@ -0,0 +1,3 @@
|
||||
If you are on a paid plan and have [Spend Cap](/docs/guides/platform/cost-control#spend-cap) disabled or your organization is on Team Plan or above, you will pay for any overages.
|
||||
|
||||
When you are exceeding your quotas while being on a Free Plan or having [Spend Cap](/docs/guides/platform/cost-control#spend-cap) enabled, you will get a notification to your billing email address and put under a grace period. For more details, refer to our [Fair Use Policy](/docs/guides/platform/billing-faq#fair-use-policy).
|
||||
@@ -1,5 +1,3 @@
|
||||
## Pricing
|
||||
|
||||
<Price price="5" /> per 1,000 origin images. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
|
||||
3. Authenticate with HTTP Basic Auth:
|
||||
|
||||
- **Username**: `service_role`
|
||||
- **Password**: a service role secret (JWT) from [**Project Settings > JWT**](/dashboard/project/_/settings/jwt) or any other Secret API key from [**Project Settings > API keys** (opens in a new tab)](/dashboard/project/_/settings/api-keys)
|
||||
- **Password**: a **Secret API key** (`sb_secret_...`). You can create/copy it in [**Project Settings → API Keys**](/dashboard/project/_/settings/api-keys). For more context, see [Understanding API keys](/docs/guides/api/api-keys).
|
||||
|
||||
Testing locally is as simple as running `curl` with your service role secret:
|
||||
Testing locally is as simple as running `curl` with your Secret API key:
|
||||
|
||||
```bash
|
||||
curl <project-url>/customer/v1/privileged/metrics \
|
||||
@@ -35,7 +35,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
|
||||
You can provision long-lived automation tokens in two ways:
|
||||
|
||||
- Create an account access token once at [**Account Settings > Access Tokens**](/dashboard/account/tokens) and reuse it wherever you configure observability tooling.
|
||||
- **Optional**: programmatically exchange an access token for project API keys via the [Management API ](/docs/reference/api/management-projects-api-keys-retrieve').
|
||||
- **Optional**: programmatically exchange an access token for project API keys via the [Management API](/docs/reference/api/management-projects-api-keys-retrieve).
|
||||
|
||||
```bash
|
||||
# (Optional) Exchange an account access token for project API keys
|
||||
|
||||
@@ -4,6 +4,10 @@ Go to [database.new](https://database.new) and create a new Supabase project.
|
||||
|
||||
Alternatively, you can create a project using the Management API:
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
# First, get your access token from https://supabase.com/dashboard/account/tokens
|
||||
export SUPABASE_ACCESS_TOKEN="your-access-token"
|
||||
@@ -24,9 +28,13 @@ curl -X POST https://api.supabase.com/v1/projects \
|
||||
}'
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details>
|
||||
|
||||
When your project is up and running, go to the [Table Editor](/dashboard/project/_/editor), create a new table and insert some data.
|
||||
|
||||
Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create a `instruments` table with some sample data.
|
||||
Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create an `instruments` table with some sample data.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
|
||||
@@ -7,6 +7,17 @@ The next step requires a callback URL, which looks like this: `https://<project-
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
#### Local development
|
||||
|
||||
When testing OAuth locally with the Supabase CLI, ensure your OAuth provider
|
||||
is configured with the local Supabase Auth callback URL:
|
||||
|
||||
http://localhost:54321/auth/v1/callback
|
||||
|
||||
If this callback URL is missing or misconfigured, OAuth sign-in may fail or not redirect correctly during local development.
|
||||
|
||||
See the [local development docs](/docs/guides/local-development) for more details.
|
||||
|
||||
For testing OAuth locally with the Supabase CLI see the [local development docs](/docs/guides/local-development).
|
||||
|
||||
</Admonition>
|
||||
@@ -29,7 +29,9 @@ Prepare you database with the relevant tables:
|
||||
|
||||
```sql
|
||||
-- Enable the pgvector extension to work with embedding vectors
|
||||
create extension vector;
|
||||
create extension vector
|
||||
with
|
||||
schema extensions;
|
||||
|
||||
-- Create a table to store your documents
|
||||
create table documents (
|
||||
|
||||
@@ -72,7 +72,7 @@ In general, embeddings with fewer dimensions perform best. See our [analysis on
|
||||
In this example we'll generate a vector using Transformers.js, then store it in the database using the Supabase JavaScript client.
|
||||
|
||||
```js
|
||||
import { pipeline } from '@xenova/transformers'
|
||||
import { pipeline } from '@huggingface/transformers'
|
||||
const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small')
|
||||
|
||||
const title = 'First post!'
|
||||
|
||||
@@ -18,6 +18,8 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
|
||||
## Community libraries
|
||||
|
||||
{/* supa-mdx-lint-disable Rule003Spelling */}
|
||||
|
||||
| `Language` | `Source Code` | `Documentation` |
|
||||
| ----------------------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](/docs/reference/csharp/introduction) |
|
||||
@@ -25,3 +27,4 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](/docs/reference/kotlin/introduction) |
|
||||
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
|
||||
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
|
||||
| Elixir | [supabase-elixir](https://github.com/supabase-community/supabase-ex) | |
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
id: 'generating-python-types'
|
||||
title: 'Generating Python Types'
|
||||
description: 'How to generate Python types for your API and Supabase libraries.'
|
||||
subtitle: 'How to generate Python types for your API and Supabase libraries.'
|
||||
---
|
||||
|
||||
Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions.
|
||||
|
||||
## Generating types using Supabase CLI
|
||||
|
||||
The Supabase CLI is a single binary Go application that provides everything you need to setup a local development environment.
|
||||
|
||||
You can [install the CLI](https://www.npmjs.com/package/supabase) via npm or other supported package managers. The minimum required version of the CLI is [v2.66.0](https://github.com/supabase/cli/releases).
|
||||
|
||||
```bash
|
||||
npm i supabase --save-dev
|
||||
```
|
||||
|
||||
Login with your Personal Access Token:
|
||||
|
||||
```bash
|
||||
npx supabase login
|
||||
```
|
||||
|
||||
Before generating types, ensure you initialize your Supabase project:
|
||||
|
||||
```bash
|
||||
npx supabase init
|
||||
```
|
||||
|
||||
Generate types for your project to produce the `database_types.py` file:
|
||||
|
||||
```bash
|
||||
npx supabase gen types --lang=python --project-id "$PROJECT_REF" --schema public > database.types.py
|
||||
```
|
||||
|
||||
or in case of local development:
|
||||
|
||||
```bash
|
||||
npx supabase gen types --lang=python --local > database_types.py
|
||||
```
|
||||
|
||||
These types are generated from your database schema. Given a table `public.movies`, the generated types will look like:
|
||||
|
||||
```sql
|
||||
create table public.movies (
|
||||
id bigint generated always as identity primary key,
|
||||
name text not null,
|
||||
data jsonb null
|
||||
);
|
||||
```
|
||||
|
||||
```py ./database_types.py
|
||||
class PublicMovies(BaseModel):
|
||||
data: Optional[Json[Any]] = Field(alias="data")
|
||||
id: int = Field(alias="id")
|
||||
name: str = Field(alias="name")
|
||||
|
||||
class PublicMoviesInsert(TypedDict):
|
||||
data: NotRequired[Annotated[Json[Any], Field(alias="data")]]
|
||||
id: NotRequired[Annotated[int, Field(alias="id")]]
|
||||
name: Annotated[str, Field(alias="name")]
|
||||
|
||||
class PublicMoviesUpdate(TypedDict):
|
||||
data: NotRequired[Annotated[Json[Any], Field(alias="data")]]
|
||||
id: NotRequired[Annotated[int, Field(alias="id")]]
|
||||
name: NotRequired[Annotated[str, Field(alias="name")]]
|
||||
```
|
||||
|
||||
## Types for select, insert and update
|
||||
|
||||
The `PublicMovies` class is used to parse `SELECT` results from the `movies` table, while `PublicMoviesInsert` and `PublicMoviesUpdate` are used to format and provide completion for arguments for `insert` and `update` respectively.
|
||||
|
||||
```py
|
||||
from .database_types import PublicMovies, PublicMoviesInsert, PublicMoviesUpdate
|
||||
from supabase import create_client
|
||||
|
||||
client = create_client("YOUR_SUPABASE_URL", "YOUR_SUPABASE_KEY")
|
||||
movies = client.table("movies")
|
||||
|
||||
movies = supabase.table("movies")
|
||||
|
||||
# Select
|
||||
selected = [PublicMovies(m) for m in movies.select("*").execute().data]
|
||||
|
||||
# Insert
|
||||
inserted = [PublicMovies(m) for m in movies.insert(PublicMoviesInsert(name="foo", data="bar")) \
|
||||
.execute().data]
|
||||
|
||||
# Update
|
||||
updated = [PublicMovies(m) for m in movies.update(PublicMoviesUpdate(name="bar")) \
|
||||
.eq("id", 5) \
|
||||
.execute().data]
|
||||
```
|
||||
|
||||
## Update types automatically with GitHub Actions
|
||||
|
||||
One way to keep your type definitions in sync with your database is to set up a GitHub action that runs on a schedule.
|
||||
|
||||
Add the following script to your `package.json` to run it using `npm run update-types`
|
||||
|
||||
```json
|
||||
"update-types": "npx supabase gen types --lang=python --project-id \"$PROJECT_REF\" > database_types.py"
|
||||
```
|
||||
|
||||
Create a file `.github/workflows/update-types.yml` with the following snippet to define the action along with the environment variables. This script will commit new type changes to your repo every night.
|
||||
|
||||
```yaml
|
||||
name: Update database types
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# sets the action to run daily. You can modify this to run the action more or less frequently
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
PROJECT_REF: <your-project-id>
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm run update-types
|
||||
- name: check for file changes
|
||||
id: git_status
|
||||
run: |
|
||||
echo "status=$(git status -s)" >> $GITHUB_OUTPUT
|
||||
- name: Commit files
|
||||
if: ${{contains(steps.git_status.outputs.status, ' ')}}
|
||||
run: |
|
||||
git add database_types.py
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "Update database types" -a
|
||||
- name: Push changes
|
||||
if: ${{contains(steps.git_status.outputs.status, ' ')}}
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.ref }}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Generating Supabase types with GitHub Actions](https://blog.esteetey.dev/how-to-create-and-test-a-github-action-that-generates-types-from-supabase-database)
|
||||
- [Generating TypeScript Types](/docs/guides/api/rest/generating-types)
|
||||