Merge branch 'master' into feature/cookbook-poc

This commit is contained in:
Saxon Fletcher committed 2026-02-04 14:46:20 +10:00
commit e2010e8949
4646 files changed
+590973 -154381

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
exit 0
fi
pnpm install
+15
View File
@@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
}
]
}
]
}
}
+233
View File
@@ -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
-267
View File
@@ -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.
-71
View File
@@ -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
+132
View File
@@ -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.
-253
View File
@@ -1,253 +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"
## 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
- Build forms with `react-hook-form` + `zod`.
- Use our `_Shadcn_` form primitives from `ui` and prefer `FormItemLayout` with layout="flex-row-reverse" for most controls (see `apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx`).
- Keep imports from `ui` with `_Shadcn_` suffixes.
- Forms should generally be wrapped in a Card unless specified
### Example (single field)
```tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const profileSchema = z.object({
username: z.string().min(2, 'Username must be at least 2 characters'),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof profileSchema>>({
resolver: zodResolver(profileSchema),
defaultValues: { username: '' },
mode: 'onSubmit',
reValidateMode: 'onBlur',
})
function onSubmit(values: z.infer<typeof profileSchema>) {
// handle values
}
return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardContent className="space-y-6">
<FormField_Shadcn_
control={form.control}
name="username"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Username"
description="This is your public display name."
>
<FormControl_Shadcn_>
<Input_Shadcn_ placeholder="shadcn" autoComplete="off" {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end">
<Button type="primary" htmlType="submit">
Submit
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
)
}
```
## 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
## 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)
+33
View File
@@ -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`
+13
View File
@@ -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.
+433
View File
@@ -0,0 +1,433 @@
---
description: "Studio: React and TypeScript best practices for maintainable Studio code"
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio Best Practices
## Boolean Handling
### Assign complex conditions to descriptive variables
When you have multiple conditions in a single expression, extract them into well-named boolean variables. This improves readability and makes the code self-documenting.
```tsx
// ❌ Bad - complex inline condition
{
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && (
<Button onClick={onAddColumn}>New column</Button>
)
}
// ✅ Good - extract to descriptive variables
const isTableEntity = isTableLike(selectedTable)
const canShowAddButton = !isSchemaLocked && isTableEntity && canUpdateColumns && !isLoading
{
canShowAddButton && <Button onClick={onAddColumn}>New column</Button>
}
```
### Use consistent naming conventions for booleans
- Use `is` prefix for state/identity: `isLoading`, `isPaused`, `isNewRecord`, `isError`
- Use `has` prefix for possession: `hasPermission`, `hasShownModal`, `hasData`
- Use `can` prefix for capability/permission: `canUpdateColumns`, `canDelete`, `canEdit`
- Use `should` prefix for conditional behavior: `shouldFetch`, `shouldRender`, `shouldValidate`
```tsx
// ✅ Good examples from codebase
const isNewRecord = column === undefined
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day'))
const { can: canUpdateColumns } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'columns'
)
```
### Derive boolean state instead of storing it
When a boolean can be computed from existing state, derive it rather than storing it separately.
```tsx
// ❌ Bad - storing derived state
const [isFormValid, setIsFormValid] = useState(false)
useEffect(() => {
setIsFormValid(name.length > 0 && email.includes('@'))
}, [name, email])
// ✅ Good - derive from existing state
const isFormValid = name.length > 0 && email.includes('@')
```
## Component Structure
### Break down large components
Components should ideally be under 200-300 lines. If a component grows larger, consider splitting it.
**Signs a component should be split:**
- Multiple distinct UI sections
- Complex conditional rendering logic
- Multiple useState hooks for unrelated state
- Difficult to understand at a glance
```tsx
// ❌ Bad - monolithic component with everything inline
const UserDashboard = () => {
// 50 lines of hooks and state
// 100 lines of handlers
// 300 lines of JSX with nested conditions
}
// ✅ Good - split into focused sub-components
const UserDashboard = () => {
return (
<div>
<UserHeader />
<UserStats />
<UserActivitySection />
<UserSettingsPanel />
</div>
)
}
```
### Co-locate related components
Place sub-components in the same directory as the parent component. Use an index file for cleaner imports.
```
components/interfaces/Auth/Users/
├── UserPanel.tsx
├── UserOverview.tsx
├── UserLogs.tsx
├── Users.constants.ts
└── index.ts
```
### Extract repeated JSX patterns
If you find yourself copying similar JSX blocks, extract them into a component.
```tsx
// ❌ Bad - repeated pattern
<TabsTrigger_Shadcn_ value="overview" className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none">
Overview
</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_ value="logs" className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none">
Logs
</TabsTrigger_Shadcn_>
// ✅ Good - extract to component
const PanelTab = ({ value, children }: { value: string; children: ReactNode }) => (
<TabsTrigger_Shadcn_
value={value}
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
>
{children}
</TabsTrigger_Shadcn_>
)
```
## Loading and Error States
### Use consistent loading/error/success pattern
Follow a consistent pattern for handling async states:
```tsx
const { data, error, isLoading, isError, isSuccess } = useQuery()
// Handle loading state first
if (isLoading) {
return <GenericSkeletonLoader />
}
// Handle error state
if (isError) {
return <AlertError error={error} subject="Failed to load data" />
}
// Handle empty state if needed
if (isSuccess && data.length === 0) {
return <EmptyState />
}
// Render success state
return <DataDisplay data={data} />
```
### Use early returns for guard clauses
Prefer early returns over deeply nested conditionals:
```tsx
// ❌ Bad - deeply nested
const Component = () => {
if (data) {
if (!isError) {
if (hasPermission) {
return <ActualContent />
}
}
}
return null
}
// ✅ Good - early returns
const Component = () => {
if (!data) return null
if (isError) return <ErrorDisplay />
if (!hasPermission) return <PermissionDenied />
return <ActualContent />
}
```
## State Management
### Keep state as local as possible
Start with local state and lift up only when needed.
```tsx
// ✅ Good - state lives where it's used
const SearchableList = () => {
const [filterString, setFilterString] = useState('')
const filteredItems = items.filter((item) => item.name.includes(filterString))
return (
<div>
<Input value={filterString} onChange={(e) => setFilterString(e.target.value)} />
<List items={filteredItems} />
</div>
)
}
```
### Group related state with objects or reducers
When you have multiple related pieces of state, consider grouping them:
```tsx
// ❌ Bad - multiple related useState calls
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
// ✅ Good - grouped state for forms (use react-hook-form)
const form = useForm<FormValues>({
defaultValues: { name: '', email: '', phone: '' },
})
```
## Custom Hooks
### Extract complex logic into custom hooks
When logic becomes reusable or complex, extract it:
```tsx
// ✅ Good - extracted to custom hook
export function useAsyncCheckPermissions(action: string, resource: string) {
const { permissions, isLoading, isSuccess } = useGetProjectPermissions()
const can = useMemo(() => {
if (!IS_PLATFORM) return true
if (!isSuccess || !permissions) return false
return doPermissionsCheck(permissions, action, resource)
}, [isSuccess, permissions, action, resource])
return { isLoading, isSuccess, can }
}
// Usage
const { can: canUpdateColumns } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'columns'
)
```
### Return objects from hooks for better extensibility
```tsx
// ❌ Bad - returning array (hard to extend)
const useToggle = () => {
const [value, setValue] = useState(false)
return [value, () => setValue((v) => !v)]
}
// ✅ Good - returning object (easy to extend)
const useToggle = (initial = false) => {
const [value, setValue] = useState(initial)
return {
value,
toggle: () => setValue((v) => !v),
setTrue: () => setValue(true),
setFalse: () => setValue(false),
}
}
```
## Event Handlers
### Name handlers consistently
Use `on` prefix for prop callbacks and `handle` prefix for internal handlers:
```tsx
interface Props {
onClose: () => void // Callback prop
onSave: (data: Data) => void
}
const Component = ({ onClose, onSave }: Props) => {
const handleSubmit = () => {
// Internal handler
// process data
onSave(data)
}
const handleCancel = () => {
// cleanup
onClose()
}
}
```
### Avoid inline arrow functions for expensive operations
```tsx
// ❌ Bad - creates new function every render
<ExpensiveList items={items} onItemClick={(item) => handleItemClick(item)} />
// ✅ Good - stable reference with useCallback
const handleItemClick = useCallback(
(item: Item) => {
// handle click
},
[dependencies]
)
<ExpensiveList items={items} onItemClick={handleItemClick} />
```
## Conditional Rendering
### Use appropriate patterns for different scenarios
```tsx
// Simple show/hide - use &&
{
isVisible && <Component />
}
// Binary choice - use ternary
{
isLoading ? <Spinner /> : <Content />
}
// Multiple conditions - use early returns or extracted component
const StatusDisplay = ({ status }: { status: Status }) => {
if (status === 'loading') return <Spinner />
if (status === 'error') return <ErrorMessage />
if (status === 'empty') return <EmptyState />
return <DataDisplay />
}
```
### Avoid nested ternaries
```tsx
// ❌ Bad - nested ternary
{
isLoading ? <Spinner /> : isError ? <Error /> : <Content />
}
// ✅ Good - separate conditions or early returns
if (isLoading) return <Spinner />
if (isError) return <Error />
return <Content />
```
## Performance
### Use useMemo for expensive computations
```tsx
// ✅ Good - memoize expensive filtering
const filteredItems = useMemo(
() => items.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase())),
[items, searchQuery]
)
```
### Avoid premature optimization
Don't wrap everything in useMemo/useCallback. Only optimize when:
- You have measured a performance problem
- The computation is genuinely expensive
- The value is passed to memoized children
## TypeScript
### Define prop interfaces explicitly
```tsx
interface UserCardProps {
user: User
onEdit: (user: User) => void
onDelete: (userId: string) => void
isEditable?: boolean
}
export const UserCard = ({ user, onEdit, onDelete, isEditable = true }: UserCardProps) => {
// ...
}
```
### Use discriminated unions for complex state
```tsx
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
```
### Avoid type casting, prefer validation with zod
Never use type casting (e.g., `as any`, `as Type`). Instead, validate values at runtime using zod schemas. This ensures type safety and catches runtime errors.
```tsx
// ❌ Bad - type casting bypasses type checking
const user = apiResponse as User
const data = unknownValue as string
// ✅ Good - validate with zod schema
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
const user = userSchema.parse(apiResponse)
const data = z.string().parse(unknownValue)
// ✅ Good - safe parsing with error handling
const result = userSchema.safeParse(apiResponse)
if (result.success) {
const user = result.data
} else {
// handle validation errors
}
```
+14
View File
@@ -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).
+26
View File
@@ -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.
+25
View File
@@ -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.
+35
View File
@@ -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.
+28
View File
@@ -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.
+19
View File
@@ -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`
+113
View File
@@ -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`.
+23
View File
@@ -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.
+16
View File
@@ -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.
+29
View File
@@ -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.
+324
View File
@@ -0,0 +1,324 @@
---
description: "Testing: Playwright E2E best practices for Studio tests (avoid flake + race conditions)"
globs:
- e2e/studio/**/*.ts
- e2e/studio/**/*.spec.ts
alwaysApply: false
---
# E2E Testing Best Practices
## Getting Context
Before writing or modifying tests, use the Playwright MCP to understand:
- Available page elements and their roles/locators
- Current page state and network activity
- Existing test patterns in the codebase
Avoid extensive code reading - let Playwright's inspection tools guide your understanding of the UI.
## Avoiding Race Conditions
### Set up API waiters BEFORE triggering actions
The most common source of flaky tests is race conditions between UI actions and API calls. Always create response waiters before clicking buttons or navigating.
```ts
// ❌ Bad - race condition: response might complete before waiter is set up
await page.getByRole('button', { name: 'Save' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
// ✅ Good - waiter is ready before action
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await page.getByRole('button', { name: 'Save' }).click()
await apiPromise
```
### Use `createApiResponseWaiter` for pre-navigation waits
When you need to wait for a response that happens during navigation:
```ts
// ✅ Good - waiter created before navigation
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
```
### Wait for multiple related API calls with Promise.all
When an action triggers multiple API calls, wait for all of them:
```ts
// ✅ Good - wait for all related API calls
const createTablePromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('query?key=table-create')
)
const tablesPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('tables?include_columns=true')
)
const entitiesPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('query?key=entity-types-')
)
await page.getByRole('button', { name: 'Save' }).click()
await Promise.all([createTablePromise, tablesPromise, entitiesPromise])
```
## Waiting Strategies
### Prefer Playwright's built-in auto-waiting
Playwright automatically waits for elements to be actionable. Use this instead of manual timeouts:
```ts
// ❌ Bad - arbitrary timeout
await page.waitForTimeout(2000)
await page.getByRole('button', { name: 'Submit' }).click()
// ✅ Good - auto-waits for element to be visible and enabled
await page.getByRole('button', { name: 'Submit' }).click()
```
### Use `expect.poll` for dynamic assertions
When waiting for state to change:
```ts
// ✅ Good - polls until condition is met
await expect
.poll(async () => {
return await page.getByLabel(`View ${tableName}`).count()
})
.toBe(0)
```
### Use `waitForSelector` with state for element lifecycle
```ts
// ✅ Good - wait for panel to close
await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })
```
### Avoid `networkidle` - use specific API waits instead
```ts
// ❌ Bad - unreliable and slow
await page.waitForLoadState('networkidle')
// ✅ Good - wait for specific API response
await waitForApiResponse(page, 'pg-meta', ref, 'tables')
```
### Use timeouts sparingly and only for non-API waits
```ts
// ✅ Acceptable - waiting for client-side debounce
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // Allow debounce to complete
// ✅ Acceptable - waiting for clipboard API
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
```
## Test Structure
### Use the custom test utility
Always import from the custom test utility for consistent fixtures:
```ts
import { test } from '../utils/test.js'
```
### Use `withFileOnceSetup` for expensive setup
When setup is expensive (cleanup, seeding), run it once per file:
```ts
test.beforeAll(async ({ browser, ref }) => {
await withFileOnceSetup(import.meta.url, async () => {
const ctx = await browser.newContext()
const page = await ctx.newPage()
// Expensive setup logic (e.g., cleanup old test data)
await deleteTestTables(page, ref)
})
})
test.afterAll(async () => {
await releaseFileOnceCleanup(import.meta.url)
})
```
### Dismiss toasts before interacting with UI
Toasts can overlay buttons and block interactions:
```ts
const dismissToastsIfAny = async (page: Page) => {
const closeButtons = page.getByRole('button', { name: 'Close toast' })
const count = await closeButtons.count()
for (let i = 0; i < count; i++) {
await closeButtons.nth(i).click()
}
}
// ✅ Good - dismiss toasts before clicking
await dismissToastsIfAny(page)
await page.getByRole('button', { name: 'New table' }).click()
```
## Assertions
### Always include descriptive messages
```ts
// ❌ Bad - no context on failure
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
// ✅ Good - clear message on failure
await expect(
page.getByRole('button', { name: 'Save' }),
'Save button should be visible after form is filled'
).toBeVisible()
```
### Use appropriate timeouts for slow operations
```ts
// ✅ Good - explicit timeout for slow operations
await expect(
page.getByText(`Table ${tableName} is good to go!`),
'Success toast should be visible after table creation'
).toBeVisible({ timeout: 50000 })
```
## Locators
### Prefer role-based locators
```ts
// ✅ Good - semantic and resilient
page.getByRole('button', { name: 'Save' })
page.getByRole('textbox', { name: 'Username' })
page.getByRole('menuitem', { name: 'Delete' })
// ❌ Avoid - brittle CSS selectors
page.locator('.btn-primary')
page.locator('#submit-button')
```
### Use test IDs for complex elements
```ts
// ✅ Good - stable identifier for complex elements
page.getByTestId('table-editor-side-panel')
page.getByTestId('action-bar-save-row')
```
### Use `filter` for finding elements in context
```ts
// ✅ Good - find button within specific row
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
await bucketRow.getByRole('button').click()
```
## Helper Functions
### Extract reusable operations into helpers
Create helper functions for common operations:
```ts
// e2e/studio/utils/storage-helpers.ts
export const createBucket = async (
page: Page,
ref: string,
bucketName: string,
isPublic: boolean = false
) => {
await navigateToStorageFiles(page, ref)
// Check if already exists
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
if ((await bucketRow.count()) > 0) return
await dismissToastsIfAny(page)
// Create bucket with proper waits
const apiPromise = waitForApiResponse(page, 'storage', ref, 'bucket', { method: 'POST' })
await page.getByRole('button', { name: 'New bucket' }).click()
await page.getByRole('textbox', { name: 'Bucket name' }).fill(bucketName)
await page.getByRole('button', { name: 'Create' }).click()
await apiPromise
await expect(
page.getByRole('row').filter({ hasText: bucketName }),
`Bucket ${bucketName} should be visible`
).toBeVisible()
}
```
### Use the existing wait utilities
```ts
import {
createApiResponseWaiter,
waitForApiResponse,
waitForGridDataToLoad,
waitForTableToLoad,
} from '../utils/wait-for-response.js'
```
## API Mocking
### Mock APIs for isolated testing
```ts
// ✅ Good - mock API response
await page.route('*/**/logs.all*', async (route) => {
await route.fulfill({ body: JSON.stringify(mockAPILogs) })
})
```
### Use soft waits for optional API calls
```ts
// ✅ Good - don't fail if API doesn't respond
await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', {
soft: true,
fallbackWaitMs: 1000,
})
```
## Cleanup
### Clean up test data in beforeAll/beforeEach
```ts
test.beforeEach(async ({ page, ref }) => {
await deleteAllBuckets(page, ref)
})
```
### Handle existing state gracefully
```ts
// ✅ Good - check before trying to delete
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
if ((await bucketRow.count()) === 0) return
// proceed with deletion
```
### Reset local storage when needed
```ts
import { resetLocalStorage } from '../utils/reset-local-storage.js'
// Clean up after tests that modify local storage
await resetLocalStorage(page, ref)
```
@@ -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)
+4 -1
View File
@@ -2,6 +2,7 @@
/packages/shared-data/pricing.ts @roryw10 @supabase/billing
/packages/shared-data/plans.ts @roryw10 @supabase/billing
/packages/common/telemetry-constants.ts @supabase/growth-eng
/packages/pg-meta @supabase/postgres @avallete
/apps/studio/ @supabase/Dashboard
@@ -11,9 +12,11 @@
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
/docker/ @supabase/dev-workflows @aantti
/docker/ @supabase/dev-workflows @supabase/self-hosted
/apps/studio/csp.js @supabase/security
/apps/studio/components/interfaces/Billing/Payment @supabase/security
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
/apps/studio/data/sql/queries/ @supabase/postgres @avallete
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
ignore:
- dependency-name: '*'
update-types:
- 'version-update:semver-major'
+4 -4
View File
@@ -32,21 +32,21 @@ jobs:
working-directory: ./packages/ai-commands
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm run typecheck
- name: Run tests
@@ -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 }}
+21 -20
View File
@@ -8,26 +8,27 @@ jobs:
permissions:
issues: write
steps:
- name: Check if organization member
id: is-org-member
uses: JamesSingleton/is-organization-member@1.0.0
with:
organization: ${{ github.repository_owner }}
username: ${{ github.event.issue.user.login }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: label-member
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
run: |
if [ ${{ steps.is-org-member.outputs.result }} != "true" ]; then
echo "User is outside of organization, labeling external"
gh issue edit "$NUMBER" --add-label "external-issue"
else
echo "User is within the organization, labeling internal"
gh issue edit "$NUMBER" --add-label "internal-issue"
fi
- name: Check if organization member
id: is-org-member
uses: JamesSingleton/is-organization-member@39c59b3b17cca4eb75c81772b95e724e2a24c025 # v1.0.0
with:
organization: ${{ github.repository_owner }}
username: ${{ github.event.issue.user.login }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: label-member
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
IS_INTERNAL: ${{ steps.is-org-member.outputs.result }}
run: |
if [ "$IS_INTERNAL" != "true" ]; then
echo "User is outside of organization, labeling external"
gh issue edit "$NUMBER" --add-label "external-issue"
else
echo "User is within the organization, labeling internal"
gh issue edit "$NUMBER" --add-label "internal-issue"
fi
triage-new:
runs-on: ubuntu-latest
permissions:
+17 -12
View File
@@ -10,9 +10,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
autofix:
runs-on: blacksmith-2vcpu-ubuntu-2404
@@ -23,32 +20,40 @@ jobs:
- name: Calculate number of commits
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
- uses: actions/checkout@v4
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.PAT_AUTOFIX }}
token: ${{ steps.app-token.outputs.token }}
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
sparse-checkout: |
packages
apps
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: pnpm
- name: Install required dependencies
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run Prettier in fix mode
run: pnpm run format
- name: Commit changes and push to existing branch
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
with:
commit_message: "ci: Autofix updates from GitHub workflow"
commit_user_name: "kevcodez"
commit_user_email: "k.grueneberg1994@gmail.com"
commit_message: 'ci: Autofix updates from GitHub workflow'
commit_user_name: 'kevcodez'
commit_user_email: 'k.grueneberg1994@gmail.com'
+2 -2
View File
@@ -17,9 +17,9 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Check out code.
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: misspell
uses: reviewdog/action-misspell@v1
uses: reviewdog/action-misspell@9daa94af4357dddb6fd3775de806bc0a8e98d3e4 # v1.26.3
with:
github_token: ${{ secrets.github_token }}
locale: 'US'
+60
View File
@@ -0,0 +1,60 @@
name: Run Braintrust evals
on:
push:
branches: [master]
pull_request:
types: [opened, synchronize, labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
pull-requests: write
contents: read
jobs:
eval:
name: Run evals
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals'))
runs-on: ubuntu-latest
timeout-minutes: 20
env:
BRAINTRUST_PROJECT_ID: ${{ secrets.BRAINTRUST_PROJECT_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
# For PR events, checkout the actual branch so Braintrust can report the correct branch name instead of detached HEAD.
# github.head_ref is the PR source branch, github.ref_name is the fallback for push events (e.g., master).
ref: ${{ github.head_ref || github.ref_name }}
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Setup Evals
run: cd apps/studio && pnpm evals:setup
- name: Run Evals
uses: braintrustdata/eval-action@v1
with:
api_key: ${{ secrets.BRAINTRUST_API_KEY }}
runtime: node
package_manager: pnpm
root: apps/studio
@@ -0,0 +1,46 @@
name: Dashboard PR Reminder
on:
schedule:
# Run at 10am Singapore Time (2am UTC)
- cron: '0 2 * * *'
# Run at 10am US Eastern Time (2pm UTC = 10am EDT / 9am EST)
- cron: '0 14 * * *'
workflow_dispatch: # Allow manual trigger for testing
permissions:
pull-requests: read
contents: read
jobs:
check-dashboard-prs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Find Dashboard PRs older than 24 hours
id: find-prs
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const findStalePRs = require('./scripts/actions/find-stale-dashboard-prs.js');
return await findStalePRs({ github, context, core });
- name: Send Slack notification
if: fromJSON(steps.find-prs.outputs.count) > 0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
STALE_PRS_JSON: ${{ steps.find-prs.outputs.stale_prs }}
with:
script: |
const sendSlackNotification = require('./scripts/actions/send-slack-pr-notification.js');
const stalePRs = JSON.parse(process.env.STALE_PRS_JSON);
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
await sendSlackNotification(stalePRs, webhookUrl);
- name: No stale PRs found
if: fromJSON(steps.find-prs.outputs.count) == 0
run: |
echo "✓ No Dashboard PRs older than 24 hours found"
+19 -12
View File
@@ -22,23 +22,23 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: master
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Regenerate JS client libraries tsdoc files
working-directory: apps/docs/spec
@@ -54,14 +54,21 @@ jobs:
echo "Generating new typespec snapshot for review..."
npx vitest run --update ./features/docs/Reference.typeSpec.test.ts
- name: Create pull request
uses: peter-evans/create-pull-request@v6
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
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 }})'
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: ${{ 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'
+4 -4
View File
@@ -23,24 +23,24 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Update last-changed dates
working-directory: ./apps/docs
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- id: download_artifact
name: 'Download artifact'
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -53,7 +53,7 @@ jobs:
run: unzip lint_results.zip
- name: 'Comment on PR'
if: steps.download_artifact.outputs.contains_results == 'true'
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const fs = require('fs');
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
lint-all:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
sparse-checkout: |
@@ -24,7 +24,7 @@ jobs:
apps/docs/content
- name: cache cargo
id: cache-cargo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/bin/
+3 -3
View File
@@ -28,14 +28,14 @@ jobs:
name: supa-mdx-lint
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 #v3.0.2
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
@@ -98,7 +98,7 @@ jobs:
fi
- name: save results as artifact (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: lint_results
path: __github_actions__pr/
+14 -7
View File
@@ -15,34 +15,41 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: master
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Create pull request
uses: peter-evans/create-pull-request@v6
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
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: ${{ 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.'
@@ -0,0 +1,87 @@
# Sync AI-generated troubleshooting guides from supabase/troubleshooting
name: Sync from supabase/troubleshooting
on:
repository_dispatch:
types: [sync_from_upstream]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout supabase/supabase
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: true
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Decode the GitHub App Private Key
id: decode
run: |
private_key=$(echo "${{ secrets.DOCS_GITHUB_APP_PRIVATE_KEY }}" | base64 --decode | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null
echo "::add-mask::$private_key"
echo "private-key=$private_key" >> "$GITHUB_OUTPUT"
- name: Create GitHub App token for supabase/troubleshooting
id: app-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ vars.DOCS_GITHUB_APP_ID }}
private-key: ${{ steps.decode.outputs.private-key }}
repositories: troubleshooting
permission-contents: read
- name: Checkout supabase/troubleshooting
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
token: ${{ steps.app-token.outputs.token }}
repository: supabase/troubleshooting
path: troubleshooting-upstream
- name: Sync supabase/troubleshooting changes back to supabase/supabase
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name 'github-docs-bot'
git config user.email 'github-docs-bot@supabase.com'
BRANCH_NAME="bot/sync-troubleshooting"
EXISTING_BRANCH=$(git ls-remote --heads origin $BRANCH_NAME)
if [[ -n "$EXISTING_BRANCH" ]]; then
git push origin --delete $BRANCH_NAME
fi
git checkout -b $BRANCH_NAME
rsync --archive --verbose --ignore-existing ./troubleshooting-upstream/guides/ ./apps/docs/content/troubleshooting/
pnpm format
git add apps/docs/content/troubleshooting/
if git diff --quiet --cached; then
echo "No changes to sync"
exit 0
fi
git commit --message "Sync from supabase/troubleshooting"
git push origin $BRANCH_NAME
if gh pr list --state open --head $BRANCH_NAME --json number --jq '.[0].number' | grep -q .; then
gh pr comment "$BRANCH_NAME" --body "Updated troubleshooting sync with latest changes."
else
gh pr create --title "[bot] Sync from supabase/troubleshooting" --body "This PR syncs the latest troubleshooting guides from the supabase/troubleshooting repository." --head $BRANCH_NAME
fi
@@ -24,27 +24,27 @@ jobs:
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run Troubleshooting script
run: pnpm run -F docs troubleshooting:sync
run: pnpm run --filter docs troubleshooting:sync
- name: Create Pull Request
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
@@ -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'
+5 -5
View File
@@ -26,25 +26,25 @@ jobs:
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run sync script
run: pnpm run -F docs sync
run: pnpm run --filter docs sync
+4 -4
View File
@@ -19,25 +19,25 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm --prefix="apps/docs" run test:smoke
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs
@@ -31,19 +31,19 @@ jobs:
packages
supabase
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run tests
run: |
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Comment on PR
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const commentBody = `
+14 -7
View File
@@ -11,18 +11,25 @@ concurrency:
permissions:
contents: read
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: master
- uses: sobolevn/misspell-fixer-action@master
- uses: peter-evans/create-pull-request@v4
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
- uses: sobolevn/misspell-fixer-action@06ff0b508d4f4c0ba70d15f9a628232c0aade536 # v0.1.0
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ steps.app-token.outputs.token }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v6
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
+4 -4
View File
@@ -22,19 +22,19 @@ jobs:
id-token: write
steps:
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1
- uses: docker/login-action@v2
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
registry: public.ecr.aws
- uses: docker/login-action@v2
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: akhilerm/tag-push-action@v2.1.0
- uses: akhilerm/tag-push-action@85bf542f43f5f2060ef76262a67ee3607cb6db37 # v2.1.0
with:
src: docker.io/supabase/studio:${{ inputs.version }}
dst: |
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Setup the Supabase CLI
uses: supabase/setup-cli@v1
uses: supabase/setup-cli@b60b5899c73b63a2d2d651b1e90db8d4c9392f51 # v1.6.0
with:
version: latest
+5 -5
View File
@@ -24,31 +24,31 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
packages/pg-meta
packages/tsconfig
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm --filter=@supabase/pg-meta run test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3.1.6
with:
directory: packages/pg-meta/coverage
flags: pg-meta
+12 -12
View File
@@ -18,21 +18,21 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: apps
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
pnpm i
pnpm install --frozen-lockfile
- name: Run prettier
run: |-
pnpm run test:prettier
@@ -42,22 +42,22 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
i18n
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
pnpm i
pnpm install --frozen-lockfile
- name: Run prettier
run: |-
pnpm exec prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -66,23 +66,23 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs/pages
apps/docs/content
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
pnpm i
pnpm install --frozen-lockfile
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
+18 -12
View File
@@ -13,7 +13,7 @@ jobs:
image_version: ${{ steps.meta.outputs.version }}
steps:
- id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
supabase/studio
@@ -33,23 +33,23 @@ jobs:
image_digest: ${{ steps.build.outputs.digest }}
steps:
- id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
supabase/studio
tags: |
type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }}
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- id: build
uses: docker/build-push-action@v3
uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.3.1
with:
push: true
context: '{{defaultContext}}'
@@ -69,22 +69,22 @@ jobs:
outputs:
image_digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
supabase/studio
tags: |
type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }}
- uses: docker/login-action@v2
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
with:
driver: docker
driver-opts: |
@@ -92,7 +92,7 @@ jobs:
network=host
- id: build
uses: docker/build-push-action@v3
uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.3.1
with:
push: true
context: .
@@ -109,9 +109,9 @@ jobs:
- release_arm
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- uses: docker/login-action@v2
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -124,6 +124,12 @@ jobs:
docker buildx imagetools create -t supabase/studio:latest \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
echo "Published Registry Images" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Image | Link |" >> $GITHUB_STEP_SUMMARY
echo "|-------|------|" >> $GITHUB_STEP_SUMMARY
echo "| \`supabase/studio:${{ needs.settings.outputs.image_version }}\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=${{ needs.settings.outputs.image_version }}) |" >> $GITHUB_STEP_SUMMARY
echo "| \`supabase/studio:latest\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=latest) |" >> $GITHUB_STEP_SUMMARY
publish:
needs:
+4 -4
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/docs
@@ -49,18 +49,18 @@ jobs:
packages
supabase
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Update embeddings
working-directory: ./apps/docs
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
docker/
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Label Stale Issues
uses: actions/stale@v9.0.0
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
days-before-issue-stale: 30
stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.'
+31 -142
View File
@@ -2,18 +2,7 @@ name: Selfhosted Studio E2E Tests
on:
push:
branches: [master]
paths:
- 'packages/pg-meta/**/*'
- 'apps/studio/**'
- 'e2e/studio/**'
- 'pnpm-lock.yaml'
pull_request:
paths:
- 'packages/pg-meta/**/*'
- 'apps/studio/**'
- 'e2e/studio/**'
- 'pnpm-lock.yaml'
- '.github/workflows/studio-e2e-test.yml'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -27,75 +16,52 @@ permissions:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
# Require approval only for external contributors
environment: ${{ github.event.pull_request.author_association != 'MEMBER' && 'Studio E2E Tests' || '' }}
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
EMAIL: ${{ secrets.CI_EMAIL }}
PASSWORD: ${{ secrets.CI_PASSWORD }}
NEXT_PUBLIC_API_URL: https://api.supabase.green
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
# Studio Self-Hosted project ID
VERCEL_PROJECT_ID: prj_CnatEuo7L6bUZAgmujMrm5P1rxtv
NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001
VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- uses: actions/checkout@v4
- name: Verify Vercel bypass secret exists
run: |
if [ -z "${{ secrets.VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO }}" ]; then
echo "Required secret VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO is not set" >&2
exit 1
fi
- uses: pnpm/action-setup@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
studio:
- 'packages/pg-meta/**'
- 'apps/studio/**'
- 'apps/ui-library/**'
- 'apps/design-system/**'
- 'e2e/studio/**'
- 'pnpm-lock.yaml'
- '.github/workflows/studio-e2e-test.yml'
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
if: steps.filter.outputs.studio == 'true'
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
if: steps.filter.outputs.studio == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
# Deploy a preview to Vercel (CLI mode) and capture the URL
- name: Install Vercel CLI
run: pnpm add --global vercel@latest
- name: Pull Vercel Environment Information (Preview)
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts for Vercel (is_platform=false)
env:
NEXT_PUBLIC_IS_PLATFORM: false
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project to Vercel and Get URL
id: deploy_vercel
run: |
DEPLOY_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "Vercel Preview URL: $DEPLOY_URL"
echo "DEPLOY_URL=$DEPLOY_URL" >> $GITHUB_OUTPUT
if: steps.filter.outputs.studio == 'true'
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
if: steps.filter.outputs.studio == 'true'
run: pnpm -C e2e/studio exec playwright install chromium --with-deps --only-shell
- name: 🚀 Run Playwright tests against Vercel Preview
if: steps.filter.outputs.studio == 'true'
id: playwright
continue-on-error: true
env:
AUTHENTICATION: false
STUDIO_URL: ${{ steps.deploy_vercel.outputs.DEPLOY_URL }}
API_URL: ${{ steps.deploy_vercel.outputs.DEPLOY_URL }}
VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO }}
run: pnpm e2e
- uses: actions/upload-artifact@v4
if: always()
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always() && steps.filter.outputs.studio == 'true'
with:
name: playwright-artifacts
path: |
@@ -103,92 +69,15 @@ jobs:
e2e/studio/test-results/
retention-days: 7
- name: Prepare summary (outputs)
if: always()
id: summarize
uses: actions/github-script@v7
- name: Comment Playwright test results on PR
uses: daun/playwright-report-comment@be9e270edd5ad86038604d3caa84a819a6ff6fed # v3.10.0
if: always() && steps.filter.outputs.studio == 'true' && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
with:
script: |
const fs = require('fs')
const p = 'e2e/studio/test-results/test-results.json'
// Initialize a summary object to hold test statistics.
let s={total:0,passed:0,failed:0,skipped:0,timedOut:0,interrupted:0,flaky:0,durationMs:0,note:''}
try {
const data = JSON.parse(fs.readFileSync(p,'utf8'))
// Recursively walk through the test suites to process each test.
const walk=suite=>{
if(!suite)return;
suite.specs?.forEach(spec=>{
spec.tests?.forEach(test=>{
s.total++;
// Get the last result of the test, as tests can be retried.
const lastResult = test.results[test.results.length - 1];
s.durationMs += lastResult.duration || 0;
// A test is considered flaky if it has more than one run and the final status is 'passed'.
if (test.results.length > 1 && lastResult.status === 'passed') {
s.flaky++
}
const status = lastResult.status === 'passed' && s.flaky > 0 ? 'flaky' : lastResult.status
s[status] = (s[status]||0)+1;
})
})
suite.suites?.forEach(walk)
}
walk(data.suites?.[0])
} catch { s.note='No JSON report found or parse error.' }
// Generate the markdown for the summary comment.
const md = s.note ? `Note: ${s.note}` : `- Total: ${s.total}\n- Passed: ${s.passed||0}\n- Failed: ${s.failed||0}\n- Skipped: ${s.skipped||0}\n- Timed out: ${s.timedOut||0}\n- Interrupted: ${s.interrupted||0}\n- Flaky: ${s.flaky||0}\n- Duration: ${(s.durationMs/1000).toFixed(1)}s`
// Set the summary and flaky_count as outputs for subsequent steps.
core.setOutput('summary', md)
core.setOutput('flaky_count', s.flaky)
- name: Comment summary on PR
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner
const repo = context.repo.repo
const issue_number = context.issue.number
const summary = `${{ steps.summarize.outputs.summary }}`.replace(/^"|"$/g,'')
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}`
const marker = '<!-- studio-e2e-summary -->'
const now = new Date()
const weekday = now.toLocaleString('en-US', { weekday: 'long', timeZone: 'UTC' })
const day = now.toLocaleString('en-US', { day: 'numeric', timeZone: 'UTC' })
const month = now.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' })
const year = now.toLocaleString('en-US', { year: 'numeric', timeZone: 'UTC' })
const time = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'UTC',
})
const date = `${weekday} ${day}, ${month}, ${year} ${time} (UTC)`
const body = [
marker,
`**Studio E2E Results**`,
'',
summary,
'',
`Artifacts: ${runUrl}`,
'',
`Last updated: ${date}`
].join('\n')
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 })
const existing = comments.find(c => c.body && c.body.includes(marker))
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body })
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body })
}
report-file: e2e/studio/test-results/test-results.json
comment-title: '🎭 Playwright Test Results'
- name: Fail job if tests failed
if: steps.playwright.outcome != 'success' || steps.summarize.outputs.flaky_count > 0
if: steps.filter.outputs.studio == 'true' && (steps.playwright.outcome != 'success' || steps.summarize.outputs.flaky_count > 0)
run: |
echo "E2E tests failed" >&2
exit 1
@@ -0,0 +1,87 @@
name: Decrease studio lint ratchet baselines
on:
schedule:
- cron: '0 0 * * SUN'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
decrease-baselines:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
.github
apps/studio
packages
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install 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: ${{ steps.app-token.outputs.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -eo pipefail
DEFAULT_BRANCH=${DEFAULT_BRANCH:-master}
BRANCH="bot/decrease-eslint-ratchet-baselines"
git fetch origin "$DEFAULT_BRANCH" --depth=1
if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
git fetch origin "$BRANCH":"$BRANCH" --depth=1
git switch "$BRANCH"
git reset --hard "origin/$DEFAULT_BRANCH"
else
git switch --create "$BRANCH" "origin/$DEFAULT_BRANCH"
fi
pnpm --filter studio run lint:ratchet --decrease-baselines
if git diff --quiet; then
echo "No baseline updates detected."
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add apps/studio/.github/eslint-rule-baselines.json
git commit --message "chore: decrease ESLint ratchet baselines"
git push --force origin "$BRANCH"
pr_url=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""' 2>/dev/null || echo "")
if [ -z "$pr_url" ]; then
gh pr create \
--title "[bot] Decrease ESLint ratchet baselines" \
--body "Automated weekly decrease of ESLint ratchet baselines." \
--base "$DEFAULT_BRANCH" \
--head "$BRANCH"
else
gh pr comment "$pr_url" --body "Updated ESLint ratchet baselines with the latest weekly decreases."
fi
+45
View File
@@ -0,0 +1,45 @@
name: Ratchet studio lint checks
on:
pull_request:
branches:
- master
paths:
- 'apps/studio/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ratchet:
# Uses larger hosted runner as it significantly decreases build times
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
.github
apps/studio
packages
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Run ratchet script
run: pnpm --filter studio run lint:ratchet
+6 -6
View File
@@ -32,22 +32,22 @@ jobs:
test_number: [1]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
apps/studio
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
working-directory: ./
- name: Run Tests
env:
@@ -57,7 +57,7 @@ jobs:
working-directory: ./apps/studio
- name: Upload coverage results to Coveralls
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
with:
parallel: true
flag-name: studio-tests
@@ -72,7 +72,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
with:
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger sync in supabase-nimbus
uses: peter-evans/repository-dispatch@v4
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
with:
token: ${{ secrets.NIMBUS_SYNC_TOKEN }}
repository: supabase/supabase-nimbus
+9 -8
View File
@@ -16,29 +16,30 @@ permissions:
jobs:
typecheck:
# Uses larger hosted runner as it significantly decreases build times
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run TypeScript type check
run: pnpm exec turbo run typecheck
run: pnpm run typecheck
- name: Run Lint
run: pnpm exec turbo run lint
run: pnpm run lint
+4 -4
View File
@@ -19,24 +19,24 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm run test:ui-patterns
+6 -6
View File
@@ -22,31 +22,31 @@ jobs:
test_number: [1]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm run test:ci
working-directory: ./packages/ui
- name: Upload coverage results to Coveralls
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
with:
parallel: true
flag-name: ui-tests
@@ -60,7 +60,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
with:
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}
+25 -14
View File
@@ -22,17 +22,17 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: master
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
@@ -41,36 +41,47 @@ jobs:
run: |
# Update @supabase/supabase-js
sed -i "s/'@supabase\/supabase-js': .*/'@supabase\/supabase-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
# Update @supabase/auth-js
sed -i "s/'@supabase\/auth-js': .*/'@supabase\/auth-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
# Update @supabase/realtime-js
sed -i "s/'@supabase\/realtime-js': .*/'@supabase\/realtime-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
# Update @supabase/postgrest-js
sed -i "s/'@supabase\/postgrest-js': .*/'@supabase\/postgrest-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
echo "Updated pnpm-workspace.yaml:"
cat pnpm-workspace.yaml
- name: Install dependencies
run: pnpm i --no-frozen-lockfile
- name: Create pull request
uses: peter-evans/create-pull-request@v6
run: pnpm install --no-frozen-lockfile
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
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: ${{ 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: |
This PR updates @supabase/*-js libraries to version ${{ github.event.inputs.version }}.
**Source**: ${{ github.event.inputs.source }}
**Changes**:
- Updated @supabase/supabase-js to ${{ github.event.inputs.version }}
- Updated @supabase/auth-js to ${{ github.event.inputs.version }}
- Updated @supabase/realtime-js to ${{ github.event.inputs.version }}
- Updated @supabase/postgest-js to ${{ github.event.inputs.version }}
- Refreshed pnpm-lock.yaml
This PR was created automatically.
branch: 'gha/auto-update-js-libs-v${{ github.event.inputs.version }}'
base: 'master'
+21
View File
@@ -0,0 +1,21 @@
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]
jobs:
validate-pr:
runs-on: ubuntu-latest
steps:
- name: Tagged with 'do not merge'
if: contains( github.event.pull_request.labels.*.name, 'do-not-merge')
run: |
echo "PR blocked: [tag: do not merge]"
exit 1
- name: All good
if: ${{ success() }}
run: |
echo "All good"
+9 -1
View File
@@ -116,6 +116,14 @@ next-env.d.ts
.idea
.vercel
# AI assistant local files
.claude/*
!.claude/settings.json
!.claude/scripts/
!.claude/skills/
.claude/skills/me-*
CLAUDE.md
#include template .env file for docker-compose
!docker/.env
!docker/supabase/.env
@@ -145,4 +153,4 @@ gcloud.json
# Sentry CLI config
**/.sentryclirc
keys.json
keys.json
-16
View File
@@ -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" }
}
]
}
+3
View File
@@ -0,0 +1,3 @@
packages/shared-data/compute-disk-limits.ts @supabase/infra
packages/shared-data/index.ts @supabase/infra
+2 -2
View File
@@ -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)
@@ -192,7 +192,7 @@ We support "federating" docs, meaning doc content can come directly from externa
Federated docs work using Next.js's build pipeline. We use `getStaticProps()` to fetch remote documentation (ie. markdown) at build time which is processed and passed to the respective page within the docs.
See the [Vecs Python source code](https://github.com/supabase/supabase/blob/master/apps/docs/pages/guides/ai/python/%5Bslug%5D.tsx) to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs.
See the [Vecs Python source code](https://github.com/supabase/supabase/tree/master/apps/docs/app/guides/ai/python/%5Bslug%5D to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs.
Some things to consider:
+10 -10
View File
@@ -96,11 +96,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<tr>
<td>JavaScript (TypeScript)</td>
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/auth-js" target="_blank" rel="noopener noreferrer">auth-js</a></td>
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
</tr>
<tr>
<td>Flutter</td>
@@ -189,11 +189,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<tr>
<td>Godot Engine (GDScript)</td>
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<!-- /notranslate -->
</table>
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
extends: ['eslint-config-supabase/next'],
}
+4
View File
@@ -0,0 +1,4 @@
const { defineConfig } = require('eslint/config')
const supabaseConfig = require('eslint-config-supabase/next')
module.exports = defineConfig([supabaseConfig])
+5 -5
View File
@@ -12,19 +12,19 @@
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint": "eslint .",
"migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"vercel-build": "pnpm migrate && pnpm build",
"typecheck_IGNORED": "tsc --noEmit"
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@payloadcms/admin-bar": "^3.52.0",
"@payloadcms/db-postgres": "^3.52.0",
"@payloadcms/live-preview-react": "^3.52.0",
"@payloadcms/next": "3.52.0",
"@payloadcms/payload-cloud": "3.52.0",
"@payloadcms/payload-cloud": "3.60.0",
"@payloadcms/plugin-form-builder": "3.52.0",
"@payloadcms/plugin-nested-docs": "3.52.0",
"@payloadcms/plugin-seo": "3.52.0",
@@ -53,9 +53,9 @@
"prism-react-renderer": "^2.3.1",
"react": "catalog:",
"react-dom": "catalog:",
"sharp": "~0.34.0",
"sharp": "~0.34.5",
"tailwind-merge": "^1.13.2",
"tsx": "^4.19.3",
"tsx": "catalog:",
"typescript": "catalog:"
},
"externals": {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
import type { BannerBlock as BannerBlockProps } from '@/payload-types'
import { cn } from '@/utilities/ui'
import React from 'react'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { QuoteBlock as QuoteBlockProps } from 'src/payload-types'
import type { QuoteBlock as QuoteBlockProps } from '@/payload-types'
import React from 'react'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { YouTubeBlock as YouTubeBlockProps } from 'src/payload-types'
import type { YouTubeBlock as YouTubeBlockProps } from '@/payload-types'
import { cn } from '@/utilities/ui'
import React from 'react'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Config } from 'src/payload-types'
import type { Config } from '@/payload-types'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Config } from 'src/payload-types'
import type { Config } from '@/payload-types'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
-1
View File
@@ -3,7 +3,6 @@
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"baseUrl": ".",
"esModuleInterop": true,
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
extends: ['eslint-config-supabase/next'],
}
+23
View File
@@ -47,3 +47,26 @@ Open [http://localhost:3003](http://localhost:3003) in your browser to see the r
### Watching for MDX changes
The `dev:full` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev` separately, you'll need to run `pnpm content:dev` in a separate terminal shell to watch for content changes.
### Adding components
The design system _references_ components rather than housing them. That’s an important distinction to make, as everything that follows here is about the documentation of components. You can add or edit components in one of these two places:
- [`packages/ui`](https://github.com/supabase/supabase/tree/master/packages/ui): basic UI components
- [`packages/ui-patterns`](https://github.com/supabase/supabase/tree/master/packages/ui-patterns): components which are built using NPM libraries or amalgamations of components from `patterns/ui`
With that out of the way, there are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include:
- `config/docs.ts`: list of components in the sidebar
- `content/docs`: the actual component documentation
- `registry/examples.ts`: list of example 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:
```bash
cd apps/design-system
pnpm build:registry
```
@@ -1,91 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'An area chart with axes'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Axes</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: -20,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} tickCount={3} />
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,77 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A simple area chart'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,100 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'An area chart with gradient fill'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Gradient</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-desktop)" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-mobile)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-mobile)" stopOpacity={0.1} />
</linearGradient>
</defs>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,100 +0,0 @@
'use client'
import { TrendingDown, TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from 'ui'
export const description = 'An area chart with icons'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
icon: TrendingDown,
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
icon: TrendingUp,
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Icons</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,238 +0,0 @@
'use client'
import * as React from 'react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from 'ui'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
export const description = 'An interactive area chart'
const chartData = [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
{ date: '2024-04-02', desktop: 97, mobile: 180 },
{ date: '2024-04-03', desktop: 167, mobile: 120 },
{ date: '2024-04-04', desktop: 242, mobile: 260 },
{ date: '2024-04-05', desktop: 373, mobile: 290 },
{ date: '2024-04-06', desktop: 301, mobile: 340 },
{ date: '2024-04-07', desktop: 245, mobile: 180 },
{ date: '2024-04-08', desktop: 409, mobile: 320 },
{ date: '2024-04-09', desktop: 59, mobile: 110 },
{ date: '2024-04-10', desktop: 261, mobile: 190 },
{ date: '2024-04-11', desktop: 327, mobile: 350 },
{ date: '2024-04-12', desktop: 292, mobile: 210 },
{ date: '2024-04-13', desktop: 342, mobile: 380 },
{ date: '2024-04-14', desktop: 137, mobile: 220 },
{ date: '2024-04-15', desktop: 120, mobile: 170 },
{ date: '2024-04-16', desktop: 138, mobile: 190 },
{ date: '2024-04-17', desktop: 446, mobile: 360 },
{ date: '2024-04-18', desktop: 364, mobile: 410 },
{ date: '2024-04-19', desktop: 243, mobile: 180 },
{ date: '2024-04-20', desktop: 89, mobile: 150 },
{ date: '2024-04-21', desktop: 137, mobile: 200 },
{ date: '2024-04-22', desktop: 224, mobile: 170 },
{ date: '2024-04-23', desktop: 138, mobile: 230 },
{ date: '2024-04-24', desktop: 387, mobile: 290 },
{ date: '2024-04-25', desktop: 215, mobile: 250 },
{ date: '2024-04-26', desktop: 75, mobile: 130 },
{ date: '2024-04-27', desktop: 383, mobile: 420 },
{ date: '2024-04-28', desktop: 122, mobile: 180 },
{ date: '2024-04-29', desktop: 315, mobile: 240 },
{ date: '2024-04-30', desktop: 454, mobile: 380 },
{ date: '2024-05-01', desktop: 165, mobile: 220 },
{ date: '2024-05-02', desktop: 293, mobile: 310 },
{ date: '2024-05-03', desktop: 247, mobile: 190 },
{ date: '2024-05-04', desktop: 385, mobile: 420 },
{ date: '2024-05-05', desktop: 481, mobile: 390 },
{ date: '2024-05-06', desktop: 498, mobile: 520 },
{ date: '2024-05-07', desktop: 388, mobile: 300 },
{ date: '2024-05-08', desktop: 149, mobile: 210 },
{ date: '2024-05-09', desktop: 227, mobile: 180 },
{ date: '2024-05-10', desktop: 293, mobile: 330 },
{ date: '2024-05-11', desktop: 335, mobile: 270 },
{ date: '2024-05-12', desktop: 197, mobile: 240 },
{ date: '2024-05-13', desktop: 197, mobile: 160 },
{ date: '2024-05-14', desktop: 448, mobile: 490 },
{ date: '2024-05-15', desktop: 473, mobile: 380 },
{ date: '2024-05-16', desktop: 338, mobile: 400 },
{ date: '2024-05-17', desktop: 499, mobile: 420 },
{ date: '2024-05-18', desktop: 315, mobile: 350 },
{ date: '2024-05-19', desktop: 235, mobile: 180 },
{ date: '2024-05-20', desktop: 177, mobile: 230 },
{ date: '2024-05-21', desktop: 82, mobile: 140 },
{ date: '2024-05-22', desktop: 81, mobile: 120 },
{ date: '2024-05-23', desktop: 252, mobile: 290 },
{ date: '2024-05-24', desktop: 294, mobile: 220 },
{ date: '2024-05-25', desktop: 201, mobile: 250 },
{ date: '2024-05-26', desktop: 213, mobile: 170 },
{ date: '2024-05-27', desktop: 420, mobile: 460 },
{ date: '2024-05-28', desktop: 233, mobile: 190 },
{ date: '2024-05-29', desktop: 78, mobile: 130 },
{ date: '2024-05-30', desktop: 340, mobile: 280 },
{ date: '2024-05-31', desktop: 178, mobile: 230 },
{ date: '2024-06-01', desktop: 178, mobile: 200 },
{ date: '2024-06-02', desktop: 470, mobile: 410 },
{ date: '2024-06-03', desktop: 103, mobile: 160 },
{ date: '2024-06-04', desktop: 439, mobile: 380 },
{ date: '2024-06-05', desktop: 88, mobile: 140 },
{ date: '2024-06-06', desktop: 294, mobile: 250 },
{ date: '2024-06-07', desktop: 323, mobile: 370 },
{ date: '2024-06-08', desktop: 385, mobile: 320 },
{ date: '2024-06-09', desktop: 438, mobile: 480 },
{ date: '2024-06-10', desktop: 155, mobile: 200 },
{ date: '2024-06-11', desktop: 92, mobile: 150 },
{ date: '2024-06-12', desktop: 492, mobile: 420 },
{ date: '2024-06-13', desktop: 81, mobile: 130 },
{ date: '2024-06-14', desktop: 426, mobile: 380 },
{ date: '2024-06-15', desktop: 307, mobile: 350 },
{ date: '2024-06-16', desktop: 371, mobile: 310 },
{ date: '2024-06-17', desktop: 475, mobile: 520 },
{ date: '2024-06-18', desktop: 107, mobile: 170 },
{ date: '2024-06-19', desktop: 341, mobile: 290 },
{ date: '2024-06-20', desktop: 408, mobile: 450 },
{ date: '2024-06-21', desktop: 169, mobile: 210 },
{ date: '2024-06-22', desktop: 317, mobile: 270 },
{ date: '2024-06-23', desktop: 480, mobile: 530 },
{ date: '2024-06-24', desktop: 132, mobile: 180 },
{ date: '2024-06-25', desktop: 141, mobile: 190 },
{ date: '2024-06-26', desktop: 434, mobile: 380 },
{ date: '2024-06-27', desktop: 448, mobile: 490 },
{ date: '2024-06-28', desktop: 149, mobile: 200 },
{ date: '2024-06-29', desktop: 103, mobile: 160 },
{ date: '2024-06-30', desktop: 446, mobile: 400 },
]
const chartConfig = {
visitors: {
label: 'Visitors',
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function Component() {
const [timeRange, setTimeRange] = React.useState('90d')
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const now = new Date()
let daysToSubtract = 90
if (timeRange === '30d') {
daysToSubtract = 30
} else if (timeRange === '7d') {
daysToSubtract = 7
}
now.setDate(now.getDate() - daysToSubtract)
return date >= now
})
return (
<Card>
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1 text-center sm:text-left">
<CardTitle>Area Chart - Interactive</CardTitle>
<CardDescription>Showing total visitors for the last 3 months</CardDescription>
</div>
<Select_Shadcn_ value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger_Shadcn_
className="w-[160px] rounded-lg sm:ml-auto"
aria-label="Select a value"
>
<SelectValue_Shadcn_ placeholder="Last 3 months" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_ className="rounded-xl">
<SelectItem_Shadcn_ value="90d" className="rounded-lg">
Last 3 months
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="30d" className="rounded-lg">
Last 30 days
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="7d" className="rounded-lg">
Last 7 days
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
<AreaChart data={filteredData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-desktop)" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-mobile)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-mobile)" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
stroke="var(--color-desktop)"
stackId="a"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}
@@ -1,98 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from 'ui'
export const description = 'An area chart with a legend'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Legend</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,80 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A linear area chart'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Linear</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" hideLabel />}
/>
<Area
dataKey="desktop"
type="linear"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,104 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A stacked area chart with expand stacking'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80, other: 45 },
{ month: 'February', desktop: 305, mobile: 200, other: 100 },
{ month: 'March', desktop: 237, mobile: 120, other: 150 },
{ month: 'April', desktop: 73, mobile: 190, other: 50 },
{ month: 'May', desktop: 209, mobile: 130, other: 100 },
{ month: 'June', desktop: 214, mobile: 140, other: 160 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
other: {
label: 'Other',
color: 'hsl(var(--chart-3))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Stacked Expanded</CardTitle>
<CardDescription>Showing total visitors for the last 6months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
top: 12,
}}
stackOffset="expand"
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="other"
type="natural"
fill="var(--color-other)"
fillOpacity={0.1}
stroke="var(--color-other)"
stackId="a"
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,90 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A stacked area chart'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Stacked</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dot" />} />
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,78 +0,0 @@
'use client'
import { Activity, TrendingUp } from 'lucide-react'
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A step area chart'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
icon: Activity,
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Area Chart - Step</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Area
dataKey="desktop"
type="step"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex w-full items-start gap-2 text-sm">
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="flex items-center gap-2 leading-none text-muted-foreground">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
)
}
@@ -1,94 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Bar, BarChart, CartesianGrid, Rectangle, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A bar chart with an active bar'
const chartData = [
{ browser: 'chrome', visitors: 187, fill: 'var(--color-chrome)' },
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
{ browser: 'firefox', visitors: 275, fill: 'var(--color-firefox)' },
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
]
const chartConfig = {
visitors: {
label: 'Visitors',
},
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
},
firefox: {
label: 'Firefox',
color: 'hsl(var(--chart-3))',
},
edge: {
label: 'Edge',
color: 'hsl(var(--chart-4))',
},
other: {
label: 'Other',
color: 'hsl(var(--chart-5))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Bar Chart - Active</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="browser"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => chartConfig[value as keyof typeof chartConfig]?.label}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar
dataKey="visitors"
strokeWidth={2}
radius={8}
activeIndex={2}
activeBar={({ ...props }) => {
return (
<Rectangle
{...props}
fillOpacity={0.8}
stroke={props.payload.fill}
strokeDasharray={4}
strokeDashoffset={4}
/>
)
}}
/>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}
@@ -1,60 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A bar chart'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Bar Chart</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}
@@ -1,68 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Bar, BarChart, XAxis, YAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A horizontal bar chart'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Bar Chart - Horizontal</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
margin={{
left: -20,
}}
>
<XAxis type="number" dataKey="desktop" hide />
<YAxis
dataKey="month"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={5} />
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}
@@ -1,92 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A bar chart with a custom label'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
label: {
color: 'hsl(var(--background))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Bar Chart - Custom Label</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
margin={{
right: 16,
}}
>
<CartesianGrid horizontal={false} />
<YAxis
dataKey="month"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
hide
/>
<XAxis dataKey="desktop" type="number" hide />
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Bar dataKey="desktop" layout="vertical" fill="var(--color-desktop)" radius={4}>
<LabelList
dataKey="month"
position="insideLeft"
offset={8}
className="fill-[--color-label]"
fontSize={12}
/>
<LabelList
dataKey="desktop"
position="right"
offset={8}
className="fill-foreground"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}
@@ -1,68 +0,0 @@
'use client'
import { TrendingUp } from 'lucide-react'
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
export const description = 'A bar chart with a label'
const chartData = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 73 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function Component() {
return (
<Card>
<CardHeader>
<CardTitle>Bar Chart - Label</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart
accessibilityLayer
data={chartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={8}>
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
)
}
Loaded 100 of 4646 files, more files were not shown because too many files have changed in this diff. Show more