Merge branch 'www-refresh' into docs-font-update

This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2026-06-19 16:33:29 +02:00
commit bbfcb668b6
2625 files changed
+151207 -950705

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ pnpm 10 + Turborepo monorepo. Requires Node >= 22.
| Directory | Purpose |
| ----------------- | ------------------------------------------------------------ |
| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 18 |
| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 19 |
| `apps/docs` | Documentation site |
| `apps/www` | Marketing website |
| `packages/ui` | Shared UI components (shadcn/ui based) |
+67 -5
View File
@@ -279,7 +279,7 @@ function MyBadComponent() {
### Round-tripping SQL from the database (NOT snippet content)
```ts
// ✅ GOOD: SQL from the database is promoted to SafeSqlFragment at the point
// ✅ GOOD: SQL from the database is promoted to SafeSqlFragment at the point
// of fetching
// data/function-definitions.ts
@@ -323,10 +323,10 @@ function MyComponent() {
### Snippet content is ALWAYS UNSAFE
Snippets are auto-persisted to the database and can be created or modified
through externally influenceable channels (e.g., prefilled from URL params).
The `unchecked_sql` property is typed as `UntrustedSqlFragment` to enforce this
— it must only be promoted to `SafeSqlFragment` via `acceptUntrustedSql` in an
Snippets are auto-persisted to the database and can be created or modified
through externally influenceable channels (e.g., prefilled from URL params).
The `unchecked_sql` property is typed as `UntrustedSqlFragment` to enforce this
— it must only be promoted to `SafeSqlFragment` via `acceptUntrustedSql` in an
event handler that requires explicit user action.
```ts
@@ -376,3 +376,65 @@ function SnippetRunner({ snippet }: { snippet: Snippet }) {
)
}
```
## Analytics SQL (BigQuery / ClickHouse)
The same security model applies to analytics queries, which target BigQuery
or ClickHouse via the
`/platform/projects/{ref}/analytics/endpoints/logs.all{,.otel}` endpoints.
Filter keys and values from URL parameters and UI inputs are spliced into SQL
that runs against the project's logs, so the same injection risk exists.
The brand and helpers live in `apps/studio/data/logs/safe-analytics-sql.ts`,
intentionally **disjoint** from the pg-meta `SafeSqlFragment` brand:
- `SafeLogSqlFragment` — branded type for analytics SQL.
- `safeSql` — template tag that only accepts `SafeLogSqlFragment`
interpolations.
- `analyticsLiteral(value)` — sanitizes string/number/boolean literals.
- `quotedIdent(name)` — validates and backtick-quotes dotted identifiers.
- `keyword(value, allowed)` — validates against an allow-list of operators.
- `joinSqlFragments(fragments, separator)` — composes already-branded
fragments.
The brands are kept separate because escape semantics differ — Postgres-safe
`E'…'` strings, `::jsonb` casts, and double-quoted identifiers are unsafe for
BigQuery and/or ClickHouse, and vice versa. Crossing the brands would silently
emit unsafe SQL.
The wire-boundary wrapper is `executeAnalyticsSql` in
`apps/studio/data/logs/execute-analytics-sql.ts`, analogous to pg-meta's
`executeSql`. It accepts only `SafeLogSqlFragment` for its `sql` parameter, so
raw strings are rejected at compile time. A grep-based vitest
(`apps/studio/tests/unit/lints/analytics-sql-boundary.test.ts`) prevents
regressions by failing the build if any file outside
`execute-analytics-sql.ts` calls `post()` or `get()` directly against
`logs.all` or `logs.all.otel`.
```ts
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { analyticsLiteral, quotedIdent, safeSql } from '@/data/logs/safe-analytics-sql'
// ✅ GOOD: every interpolation is sanitized.
const sql = safeSql`
SELECT timestamp, event_message
FROM ${quotedIdent(table)}
WHERE id = ${analyticsLiteral(id)}
`
await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql,
iso_timestamp_start,
iso_timestamp_end,
})
```
```ts
// 🛑 BAD: raw string interpolation. This fails to type-check at the
// executeAnalyticsSql boundary because the result is `string`, not
// `SafeLogSqlFragment`.
const sql = `SELECT * FROM ${table} WHERE id = '${id}'`
await executeAnalyticsSql({ projectRef, endpoint, sql, ... })
```
@@ -0,0 +1,294 @@
---
name: studio-mock-api-tests
description: Component tests for Supabase Studio that mock API requests at the
network layer with MSW. Use when writing or reviewing a component test that
exercises a React Query hook or mutation, or when migrating an existing
test away from vi.mock('@/data/...'). Covers the customRender + addAPIMock
template and the jsdom/MSW gotchas that cost real debugging time.
---
# Studio MSW component tests
Mount a Studio component, intercept its network calls with MSW, assert
what renders and what gets sent. The infrastructure is already wired up —
this skill is the working template plus the gotchas.
## When to use
- The component (or any descendant it renders) calls a React Query hook
or mutation that hits `/platform/...`, `/v1/...`, or another endpoint
in `apps/studio/data/api.d.ts`.
- You'd otherwise be tempted to write `vi.mock('@/data/some-query', ...)`.
**Don't.** Mock the network instead — see "Why not vi.mock" below.
If the component is purely presentational with no data fetching, you
don't need MSW; render and assert directly.
## The template
```tsx
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { MyComponent } from './MyComponent'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
// Needed if the component renders inside a Sheet, Modal, Popover, or
// anything else built on Radix that uses Web Animations.
mockAnimationsApi()
describe('MyComponent', () => {
test('renders rows from the API', async () => {
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () =>
HttpResponse.json<OrganizationResponse[]>([
{
/* ... */
},
]),
})
customRender(<MyComponent />)
expect(await screen.findByText('Acme')).toBeInTheDocument()
})
})
```
That's the whole pattern. Server lifecycle (`listen`/`resetHandlers`/
`close`) is handled by `apps/studio/tests/vitestSetup.ts` — handlers
registered via `addAPIMock` are scoped to the current test.
## Gotchas that will eat your afternoon
### 1. Path params use `:slug`, not `{slug}`
`addAPIMock` is typed from the OpenAPI `paths`, but path params are
remapped to MSW's `:param` format. Autocomplete will guide you, but if
typecheck reports the path isn't assignable, you're using the OpenAPI
`{slug}` form.
```ts
// ❌ TypeScript error, MSW won't match
path: '/platform/organizations/{slug}/projects'
// ✅ Correct
path: '/platform/organizations/:slug/projects'
```
### 2. Use `HttpResponse.json`, not `new HttpResponse`
For success responses, always go through `HttpResponse.json` — even for
204/201-no-content endpoints. A raw `new HttpResponse(null, { status: 201 })`
returns no content-type, and `openapi-fetch` can hang the mutation flow,
which silently breaks `onSuccess` callbacks.
```ts
// ❌ Mutation onSuccess silently never fires
response: () => new HttpResponse(null, { status: 201 })
// ✅ Works (pass the OpenAPI body shape explicitly — see gotcha #8)
response: () => HttpResponse.json<MyResponse>({}, { status: 201 })
```
### 3. Submit buttons in Sheets/Modals need `fireEvent.click`
The convention `<Button form={FORM_ID} type="submit" />` (button
outside the form, associated by id) doesn't reliably trigger submission
under `userEvent.click` in jsdom. Use `fireEvent.click` for the submit
button. Continue to use `userEvent.type` for inputs.
```ts
await userEvent.type(screen.getByPlaceholderText('value'), 'hello')
fireEvent.click(await screen.findByRole('button', { name: 'Save' }))
```
### 4. Profile-gated queries need a `profileContext`
Many hooks (`useOrganizationsQuery`, anything in `data/projects/`,
anything that calls `useProfile`) refuse to fire until a profile is
loaded. Pass one explicitly:
```ts
import type { ProfileContextType } from '@/lib/profile'
const PROFILE_CONTEXT: ProfileContextType = {
profile: {
id: 1,
auth0_id: 'auth0|test',
gotrue_id: 'gotrue-test',
username: 'testuser',
primary_email: 'test@example.com',
first_name: null,
last_name: null,
mobile: null,
is_alpha_user: false,
is_sso_user: false,
disabled_features: [],
free_project_limit: null,
},
error: null,
isLoading: false,
isError: false,
isSuccess: true,
}
customRender(<MyComponent />, { profileContext: PROFILE_CONTEXT })
```
### 5. `useParams` is globally mocked to `{ ref: 'default' }`
You don't need to mock the Next router for project-scoped components.
Just use `'default'` as the project ref in your mock paths:
`/v1/projects/default/secrets`, `/platform/projects/default/...`. If
you need a different ref, override with `routerMock.setCurrentUrl(...)`
(see `apps/studio/tests/lib/route-mock.ts`).
### 6. Unhandled requests fail loudly — mock every endpoint a render triggers
`mswServer.listen({ onUnhandledRequest: 'error' })` is set globally. If a
component (or any child it renders) fires an unmocked request, you'll see
MSW errors in stderr and likely flaky behavior. Cards, lists, and details
panels often fire nested queries (e.g. `OrganizationCard` calls
`useOrgProjectsInfiniteQuery`) — read what the rendered subtree does and
mock all of it, or stub it with `vi.mock` for nested components only.
### 7. Don't put query strings in the handler `path`
`addAPIMock` accepts `?foo=bar` suffixes via `TrimQueryParams`, but the
helper strips them before matching. MSW v2 doesn't match query params via
path strings — read them inside the resolver instead:
```ts
addAPIMock({
method: 'get',
path: '/platform/projects',
response: ({ request }) => {
const limit = new URL(request.url).searchParams.get('limit')
// ...
},
})
```
### 8. Always pass an explicit generic to `HttpResponse.json`
`addAPIMock`'s resolver is typed against the OpenAPI success body (and the
standard `{ message: string }` error envelope, exported as `APIErrorBody`).
But MSW's `HttpResponse.json` uses `NoInfer`, so the body type doesn't
narrow from context. Pass the expected shape explicitly — it doubles as a
self-documenting contract assertion:
```ts
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
response: () => HttpResponse.json<OrganizationResponse[]>([...])
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Boom' }, { status: 500 })
```
A mock that drifts from the contract (wrong envelope, missing fields,
stale enum values) now fails at compile time, not at runtime. The cost is
one type annotation per resolver — well worth it.
For mocks at the network boundary, also prefer `createMockOrganizationResponse`
(returns the raw OpenAPI `OrganizationResponse`) over `createMockOrganization`
(which extends with frontend-derived `managed_by` / `partner_id` that the
query layer attaches). Same pattern applies to any type that's a frontend
extension of an OpenAPI schema: build a `createMockXResponse` helper that
returns the raw API shape.
## Prefer asserting on UI state
MSW's own best-practices doc explicitly recommends asserting on what
renders, not on whether a handler was called. The "did the form
submit?" question is best answered by `expect(onClose).toHaveBeenCalled()`
or by `findByText('Saved')` — not by spying on the resolver.
There's one legitimate exception: **the request body itself is the
contract you care about**, and the server's reply doesn't reflect it
back. Bulk-create endpoints (like `POST /v1/projects/:ref/secrets`) are
the canonical case — 201 with no body, so the only way to verify the
shape sent is to capture it:
```ts
const requests: Array<{ ref: string | undefined; body: unknown }> = []
addAPIMock({
method: 'post',
path: '/v1/projects/:ref/secrets',
response: async ({ request, params }) => {
requests.push({ ref: params.ref as string | undefined, body: await request.json() })
return HttpResponse.json<CreateSecretsResponse>({}, { status: 201 })
},
})
// ... drive the UI ...
expect(requests).toEqual([{ ref: 'default', body: [{ name: 'API_KEY', value: 'new-value' }] }])
```
When in doubt, assert on the UI first; reach for request capture only
when the UI doesn't observably encode the contract.
## Debugging an MSW test
If a request isn't being matched, wire up MSW's lifecycle events at the
top of the test file (or temporarily in `msw.ts`):
```ts
import { mswServer } from '@/tests/lib/msw'
mswServer.events.on('request:unhandled', ({ request }) => {
console.log('[MSW] UNHANDLED:', request.method, request.url)
})
mswServer.events.on('response:mocked', ({ request, response }) => {
console.log('[MSW] MATCHED:', request.method, request.url, response.status)
})
```
`request:start` is already wired in `msw.ts`. Add `request:unhandled` and
`response:mocked` locally when a test misbehaves — usually surfaces a
path-param mismatch or a nested query you forgot to mock.
## Why not `vi.mock('@/data/...')`
It bypasses the network boundary, so:
- It hides real bugs: a renamed query key or a changed request payload
passes the test, then breaks in production.
- It doesn't exercise React Query's caching, retry, or invalidation
paths — `onMutate`, `onSuccess`, and `onError` callbacks won't run as
they do in real life. ([tkdodo.eu/blog/testing-react-query](https://tkdodo.eu/blog/testing-react-query))
- It drifts independently from the OpenAPI types — handlers stay in sync,
module-level mocks don't.
Reach for `vi.mock` only for non-network concerns: a heavy child
component (e.g. a Monaco editor) you want to stub, or a `common`-package
hook with global state.
## Further reading
- [TkDodo — Testing React Query](https://tkdodo.eu/blog/testing-react-query) —
canonical reference for the principles behind everything in this skill.
- [MSW best practices: structuring handlers](https://mswjs.io/docs/best-practices/structuring-handlers/)
and [overriding network behavior](https://mswjs.io/docs/best-practices/network-behavior-overrides/) —
the baseline-handlers + per-test-`server.use()` pattern.
- [MSW best practices: avoid request assertions](https://mswjs.io/docs/best-practices/avoid-request-assertions/) —
the source of the "assert on UI state" guidance above.
## Codebase references
| What | Where |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Query-only example (loading, error, success) | `apps/studio/components/interfaces/Organization/OrgNotFound.test.tsx` |
| Mutation example (form, payload assertion) | `apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.test.tsx` |
| SQL-via-pg-meta example (POST resolver branch on `query` body) | `apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx` |
| `addAPIMock` source | `apps/studio/tests/lib/msw.ts` |
| `customRender` source | `apps/studio/tests/lib/custom-render.tsx` |
| Global handlers + lifecycle | `apps/studio/tests/lib/msw-global-api-mocks.ts`, `apps/studio/tests/vitestSetup.ts` |
| Related skills | `studio-testing` (when to write a component test at all), `studio-queries` (hook conventions), `vitest` |
@@ -1,141 +0,0 @@
---
name: use-static-effect-event
description: useStaticEffectEvent hook in Supabase Studio — a userland polyfill for
React's useEffectEvent. Use when you need to read latest state/props inside a useEffect
without re-triggering it, or when stale closures in Effects are causing bugs.
---
# useStaticEffectEvent
Located at `apps/studio/hooks/useStaticEffectEvent.ts`.
A userland polyfill for React's `useEffectEvent` (stable in React 19.2). It solves the stale closure problem: gives you a **stable callback** that always reads the latest props/state without those values triggering Effect re-runs.
## The Problem It Solves
Without it, you face two bad options inside `useEffect`:
1. **Add values to dependencies** → unnecessary Effect re-runs (teardown/reconnect)
2. **Omit from dependencies** → stale closure bugs (outdated values)
```tsx
// Problem: 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 reconnects
})
return () => connection.disconnect()
}, [roomId, theme])
```
## When to Use
1. Read latest state/props inside an Effect without re-triggering it
2. Create stable callbacks that always use current values
3. Avoid stale closures in event handlers used within Effects
### Pattern 1: Sync data without re-running on every change
```tsx
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
const exportInternal = useStaticEffectEvent(
async ({ bypassConfirmation }: { bypassConfirmation: boolean }) => {
if (!params.enabled) return
const { projectRef, connectionString, entity, totalRows } = params
// complex async logic using latest params
}
)
// Stable reference — safe to use in useCallback
const exportInDesiredFormat = useCallback(
() => exportInternal({ bypassConfirmation: false }),
[exportInternal]
)
```
### Pattern 3: Infinite scroll / pagination triggers
```tsx
const fetchNext = useStaticEffectEvent(() => {
if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
})
useEffect(fetchNext, [lastItem, fetchNext])
```
## When NOT to Use
**Don't use it to hide legitimate dependencies:**
```tsx
// ❌ Bad — roomId IS a legitimate dependency; this hides a bug
const connect = useStaticEffectEvent(() => {
const connection = createConnection(roomId)
connection.connect()
})
useEffect(() => {
connect()
}, [connect]) // Won't reconnect when roomId changes!
// ✅ roomId belongs in deps
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))
// ✅ Regular function is fine
const handleClick = () => console.log(count)
```
## Rules
1. Only call the returned function **inside Effects** (`useEffect`, `useLayoutEffect`)
2. Don't pass it to other components or hooks as a callback prop
3. Use for **non-reactive logic only** — reads values but shouldn't trigger re-runs
4. **Include it in dependency arrays** when used in `useEffect` (it's stable, won't cause re-runs)
## How It Works
```tsx
export const useStaticEffectEvent = <Callback extends Function>(callback: Callback) => {
const callbackRef = useRef(callback)
useLayoutEffect(() => {
callbackRef.current = callback // always latest
})
const eventFn = useCallback((...args: any) => {
return callbackRef.current(...args)
}, []) // stable reference
return eventFn as unknown as Callback
}
```
@@ -63,7 +63,7 @@ Reference these guidelines when:
### 4. React 19 APIs (MEDIUM)
> **⚠️ React 19+ only.** Supabase Studio currently uses React 18 — skip these patterns in Studio code.
> **⚠️ React 19+ only.** Skip these patterns if you're on React 18 or earlier.
- `react19-no-forwardref` - Don't use `forwardRef`; use `use()` instead of `useContext()`
+2
View File
@@ -4,6 +4,8 @@ updates:
directory: '/'
schedule:
interval: 'weekly'
cooldown:
default-days: 7
ignore:
- dependency-name: '*'
update-types:
+12 -2
View File
@@ -1,5 +1,15 @@
# Add 'documentation' to any change in apps/docs
# https://github.com/marketplace/actions/labeler
documentation:
- changed-files:
- any-glob-to-any-file: 'apps/docs/**/*'
- changed-files:
- any-glob-to-any-file: 'apps/docs/**/*'
# Add 'self-hosted' to any change in the self-hosting docs
self-hosted:
- changed-files:
- any-glob-to-any-file: 'apps/docs/content/guides/self-hosting/**/*'
# Add 'api-deploy-required' to any change in packages/api-types/types
api-deploy-required:
- changed-files:
- any-glob-to-any-file: 'packages/api-types/types/**'
+1
View File
@@ -34,6 +34,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
packages
patches
@@ -25,6 +25,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
# fetch only the root files and scripts folder
sparse-checkout: |
+2 -2
View File
@@ -81,9 +81,9 @@ jobs:
steps:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
+2
View File
@@ -18,6 +18,8 @@ jobs:
steps:
- name: Check out code.
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: misspell
uses: reviewdog/action-misspell@9daa94af4357dddb6fd3775de806bc0a8e98d3e4 # v1.26.3
with:
+1
View File
@@ -30,6 +30,7 @@ jobs:
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: false
# 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 }}
@@ -18,6 +18,7 @@ jobs:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Delete preview scorers from staging
@@ -24,14 +24,18 @@ jobs:
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.head_ref || github.ref_name }}
- name: Check for scorer file changes
id: changed
# On labeled events, always push. On synchronize, only push if scorer files changed.
env:
GH_EVENT_ACTION: ${{ github.event.action }}
GH_EVENT_PR_REF: ${{ github.event.pull_request.base.ref }}
run: |
if [[ "${{ github.event.action }}" == "synchronize" ]]; then
changed=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD | grep -E 'evals/scorer' || true)
if [[ "${GH_EVENT_ACTION}" == "synchronize" ]]; then
changed=$(git diff --name-only origin/${GH_EVENT_PR_REF}...HEAD | grep -E 'evals/scorer' || true)
if [ -z "$changed" ]; then
echo "No scorer files changed, skipping push"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -24,6 +24,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
@@ -19,6 +19,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
scripts
patches
+10 -8
View File
@@ -24,6 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
@@ -49,20 +50,21 @@ jobs:
echo "Regenerating tsdoc files for JS client libraries..."
echo "Source: ${SOURCE}"
echo "Version: ${VERSION}"
make
make download.tsdoc.v2
- name: Generate new typespec snapshot
- name: Refresh reference-content snapshot
working-directory: apps/docs
run: |
echo "Generating new typespec snapshot for review..."
npx vitest run --update --dir features/docs
# Using --dir (not a file path) so Vitest 4 doesn't run unrelated tests.
run: npx vitest run --update --dir scripts
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
@@ -72,7 +74,7 @@ jobs:
title: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
body: |
Updates JS sdk documentation following stable release.
Ran `make` in apps/docs/spec to regenerate tsdoc files.
Ran `make download.tsdoc.v2` in apps/docs/spec and refreshed the reference-content snapshot.
**Details:**
- **Version:** `${{ github.event.inputs.version }}`
+1
View File
@@ -26,6 +26,7 @@ jobs:
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: false
sparse-checkout: |
apps/docs
patches
@@ -18,6 +18,7 @@ jobs:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: true
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
+1
View File
@@ -31,6 +31,7 @@ jobs:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: false
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
+5 -2
View File
@@ -17,6 +17,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
sparse-checkout: |
apps/docs
@@ -43,10 +44,12 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-pull-requests: write
permission-contents: write
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
@@ -21,7 +21,7 @@ jobs:
with:
persist-credentials: true
- name: Install pnpm
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
@@ -35,19 +35,12 @@ jobs:
- 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
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.DOCS_GITHUB_APP_ID }}
private-key: ${{ steps.decode.outputs.private-key }}
client-id: ${{ vars.DOCS_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.DOCS_GITHUB_APP_PRIVATE_KEY_V2 }}
repositories: troubleshooting
permission-contents: read
@@ -61,10 +54,11 @@ jobs:
- name: Generate PR token
id: pr-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-pull-requests: write
- name: Sync supabase/troubleshooting changes back to supabase/supabase
env:
@@ -26,6 +26,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
patches
+1
View File
@@ -28,6 +28,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
packages
+1
View File
@@ -21,6 +21,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
packages
+9
View File
@@ -25,6 +25,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
examples
@@ -46,6 +47,14 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Download JS reference TypeDoc dumps
# The source dumps under apps/docs/spec/reference/<lib>/<ver>/*.json are
# gitignored — `make download.tsdoc.v2` re-fetches them from
# supabase.github.io so the reference-content snapshot test has
# something to walk.
working-directory: apps/docs/spec
run: make download.tsdoc.v2
- name: Run tests
run: |
touch .env
+3 -2
View File
@@ -18,14 +18,15 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
- 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
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
+14 -4
View File
@@ -1,10 +1,7 @@
name: 'Pull Request Labeler'
# only docs uses the labeler at the moment
on:
pull_request_target:
paths:
- 'apps/docs/**/*'
jobs:
labeler:
@@ -13,4 +10,17 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- id: label
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- name: Comment when api-deploy-required is auto-applied
if: contains(steps.label.outputs.new-labels, 'api-deploy-required')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'The `api-deploy-required` label was auto-applied to this PR because it updates the API types. Ensure that the new or updated API, if any, is deployed on production before **removing the label** and merging this PR.',
})
+3
View File
@@ -6,6 +6,9 @@ on:
version:
required: true
type: string
secrets:
PROD_AWS_ROLE:
required: true
workflow_dispatch:
inputs:
version:
+2
View File
@@ -22,6 +22,8 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup the Supabase CLI
uses: supabase/setup-cli@b60b5899c73b63a2d2d651b1e90db8d4c9392f51 # v1.6.0
+1
View File
@@ -26,6 +26,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
packages/pg-meta
packages/tsconfig
+1
View File
@@ -20,6 +20,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps
blocks
+14 -8
View File
@@ -75,7 +75,8 @@ jobs:
image_digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
@@ -122,18 +123,22 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Merge multi-arch manifests
env:
IMAGE_VERSION: ${{ needs.settings.outputs.image_version }}
x86_DIGEST: ${{ needs.release_x86.outputs.image_digest }}
ARM_DIGEST: ${{ needs.release_arm.outputs.image_digest }}
run: |
docker buildx imagetools create -t supabase/studio:${{ needs.settings.outputs.image_version }} \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
docker buildx imagetools create -t supabase/studio:${IMAGE_VERSION} \
supabase/studio@${x86_DIGEST} \
supabase/studio@${ARM_DIGEST}
docker buildx imagetools create -t supabase/studio:latest \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
supabase/studio@${x86_DIGEST} \
supabase/studio@${ARM_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:${IMAGE_VERSION}\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=${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:
@@ -144,4 +149,5 @@ jobs:
uses: ./.github/workflows/mirror.yml
with:
version: ${{ needs.settings.outputs.image_version }}
secrets: inherit
secrets:
PROD_AWS_ROLE: ${{ secrets.PROD_AWS_ROLE }}
+1
View File
@@ -42,6 +42,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
apps/www/.env.local.example
+60 -4
View File
@@ -17,18 +17,74 @@ permissions:
jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
config: [default, logs, envoy, rustfs, envoy-rustfs]
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@v4
with:
persist-credentials: false
sparse-checkout: |
docker/
- name: Run docker-compose up
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Generate keys
shell: bash
run: |
set -euo pipefail
cd docker
cp .env.example .env
sh utils/generate-keys.sh --update-env
sh utils/add-new-auth-keys.sh --update-env
- name: Start self-hosted Supabase
shell: bash
# Ensure all services can be started and healthy with default config
run: |
set -euo pipefail
cd docker
cp .env.example .env
yq -i '.services.supavisor.environment.RLIMIT_NOFILE=1024' docker-compose.yml
docker compose up --quiet-pull --wait --wait-timeout 180
if [ "${{ matrix.config }}" = "logs" ]; then
docker compose -f docker-compose.yml -f docker-compose.logs.yml up --quiet-pull --wait --wait-timeout 180
elif [ "${{ matrix.config }}" = "envoy" ]; then
docker compose -f docker-compose.yml -f docker-compose.envoy.yml up --quiet-pull --wait --wait-timeout 180
elif [ "${{ matrix.config }}" = "rustfs" ]; then
docker compose -f docker-compose.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180
elif [ "${{ matrix.config }}" = "envoy-rustfs" ]; then
docker compose -f docker-compose.yml -f docker-compose.envoy.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180
else
docker compose up --quiet-pull --wait --wait-timeout 180
fi
- name: Run container log tests
shell: bash
run: |
set -euo pipefail
cd docker
sh tests/test-container-logs.sh
- name: Run smoke tests
shell: bash
run: |
set -euo pipefail
cd docker
sh tests/test-self-hosted.sh
- name: Run API keys and asymmetric auth tests
shell: bash
run: |
set -euo pipefail
cd docker
sh tests/test-auth-keys.sh
- name: Run S3 protocol tests
shell: bash
run: |
set -euo pipefail
cd docker
sh tests/test-s3.sh
@@ -19,6 +19,8 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
+12 -7
View File
@@ -9,10 +9,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
test:
name: 'E2E tests'
@@ -26,11 +22,15 @@ jobs:
outputs:
tests_ran: ${{ steps.filter.outputs.studio == 'true' }}
permissions:
contents: write
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
@@ -86,7 +86,7 @@ jobs:
- name: Pre-start diagnostics
run: |
docker ps -a
sudo ss -tlnp | grep 5432 || echo "5432 free"
sudo ss -tlnp | grep 54322 || echo "54322 free"
- name: Start supabase
if: steps.filter.outputs.studio == 'true'
@@ -96,7 +96,7 @@ jobs:
if: failure()
run: |
docker ps -a
sudo ss -tlnp | grep 5432
sudo ss -tlnp | grep 54322 || echo "54322 not listening"
docker logs $(docker ps -aq) 2>&1 || true
- name: Build studio
@@ -128,9 +128,13 @@ jobs:
if: ${{ !cancelled() && needs.test.outputs.tests_ran == 'true' }}
needs: [test]
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -164,6 +168,7 @@ jobs:
merge-results:
name: 'E2E results'
runs-on: ubuntu-latest
permissions: {}
needs: [test]
if: ${{ !cancelled() && needs.test.outputs.tests_ran == 'true' }}
steps:
@@ -16,6 +16,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
.github
apps/studio
@@ -38,10 +39,12 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Decrease ESLint ratchet baselines and open PR
env:
@@ -74,7 +77,7 @@ jobs:
git add apps/studio/.github/eslint-rule-baselines.json
git commit --message "chore: decrease ESLint ratchet baselines"
git push --force origin "$BRANCH"
git -c credential.helper= push --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${BRANCH}"
pr_url=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""' 2>/dev/null || echo "")
if [ -z "$pr_url" ]; then
@@ -22,6 +22,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
.github
apps/studio
+2
View File
@@ -33,6 +33,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/studio
packages
@@ -83,6 +84,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/studio
patches
+2
View File
@@ -23,6 +23,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
+1
View File
@@ -21,6 +21,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
packages
patches
+2
View File
@@ -31,6 +31,7 @@ jobs:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
if: steps.filter.outputs.relevant == 'true'
with:
persist-credentials: false
sparse-checkout: |
packages
patches
@@ -72,6 +73,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
packages/ui
patches
+5 -2
View File
@@ -24,6 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
@@ -88,10 +89,12 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-pull-requests: write
permission-contents: write
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
@@ -119,4 +122,4 @@ jobs:
This PR was created automatically.
branch: 'gha/auto-update-js-libs-v${{ github.event.inputs.version }}'
base: 'master'
base: 'master'
+5 -2
View File
@@ -24,6 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
ref: master
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
@@ -51,10 +52,12 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
+7 -2
View File
@@ -5,8 +5,7 @@ on:
pull_request:
types: [opened, labeled, unlabeled, synchronize, ready_for_review]
permissions:
contents: read
permissions: {}
jobs:
validate-pr:
@@ -18,6 +17,12 @@ jobs:
echo "PR blocked: [tag: do not merge]"
exit 1
- name: Tagged with 'api-deploy-required'
if: contains( github.event.pull_request.labels.*.name, 'api-deploy-required')
run: |
echo "PR blocked: [tag: api-deploy-required] — confirm the API is deployed in production, then remove the label."
exit 1
- name: All good
if: ${{ success() }}
run: |
+1
View File
@@ -26,6 +26,7 @@ jobs:
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
sparse-checkout: |
apps/www
packages
+2
View File
@@ -31,3 +31,5 @@ apps/**/.contentlayer
packages/ui/src/components/Form/examples/PhoneProvidersSchema.json
# ignore because of <br/><br/>
apps/www/_blog/2025-07-14-supabase-ui-platform-kit.mdx
# vitest file snapshots — JSON.stringify owns the format, not Prettier
apps/docs/scripts/__snapshots__/
+154
View File
@@ -3195,6 +3195,160 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"markdown-full-example": {
name: "markdown-full-example",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-full-example")),
source: "",
files: ["registry/default/example/markdown-full-example.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-customization": {
name: "markdown-customization",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-customization")),
source: "",
files: ["registry/default/example/markdown-customization.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-headings": {
name: "markdown-headings",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-headings")),
source: "",
files: ["registry/default/example/markdown-headings.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-paragraphs": {
name: "markdown-paragraphs",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-paragraphs")),
source: "",
files: ["registry/default/example/markdown-paragraphs.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-lists": {
name: "markdown-lists",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-lists")),
source: "",
files: ["registry/default/example/markdown-lists.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-links": {
name: "markdown-links",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-links")),
source: "",
files: ["registry/default/example/markdown-links.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-inline-code": {
name: "markdown-inline-code",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-inline-code")),
source: "",
files: ["registry/default/example/markdown-inline-code.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-blockquotes": {
name: "markdown-blockquotes",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-blockquotes")),
source: "",
files: ["registry/default/example/markdown-blockquotes.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-code-blocks": {
name: "markdown-code-blocks",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-code-blocks")),
source: "",
files: ["registry/default/example/markdown-code-blocks.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-tables": {
name: "markdown-tables",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-tables")),
source: "",
files: ["registry/default/example/markdown-tables.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-images": {
name: "markdown-images",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-images")),
source: "",
files: ["registry/default/example/markdown-images.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-horizontal-rules": {
name: "markdown-horizontal-rules",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-horizontal-rules")),
source: "",
files: ["registry/default/example/markdown-horizontal-rules.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-quote-component": {
name: "markdown-quote-component",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-quote-component")),
source: "",
files: ["registry/default/example/markdown-quote-component.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"markdown-avatar-component": {
name: "markdown-avatar-component",
type: "components:example",
registryDependencies: ["markdown"],
component: React.lazy(() => import("@/registry/default/example/markdown-avatar-component")),
source: "",
files: ["registry/default/example/markdown-avatar-component.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"status-code-demo": {
name: "status-code-demo",
type: "components:example",
@@ -35,7 +35,7 @@ export function CodeBlockWrapper({
)}
>
<CollapsibleTrigger asChild>
<Button type="secondary" className="h-8 text-xs">
<Button variant="secondary" className="h-8 text-xs">
{isOpened ? 'Collapse' : expandButtonTitle}
</Button>
</CollapsibleTrigger>
@@ -54,7 +54,7 @@ export function CommandMenu({ ...props }: DialogProps) {
return (
<>
<Button
type="outline"
variant="outline"
className={cn(
`lg:flex hidden relative h-8 w-full justify-start rounded-lg bg-background text-sm font-normal text-foreground-muted shadow-none sm:pr-12 md:w-40 lg:w-64 hover:border-foreground-muted hover:bg-surface-100 hover:text-foreground-lighter
`
@@ -69,7 +69,7 @@ export function CommandMenu({ ...props }: DialogProps) {
</kbd>
</Button>
<Button
type="text"
variant="text"
size="tiny"
className="px-1 group lg:hidden"
onClick={() => setOpen(true)}
@@ -125,7 +125,7 @@ export function ComponentPreview({
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
type="default"
variant="default"
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
@@ -49,7 +49,7 @@ export function CopyButton({
return (
<Button
size="small"
type="outline"
variant="outline"
className={cn(
'relative z-10 h-6 w-6 text-foreground-muted hover:bg-surface-100 hover:text-foreground p-0',
className
@@ -106,7 +106,7 @@ export function CopyWithClassNames({
<DropdownMenuTrigger asChild>
<Button
size="small"
type="outline"
variant="outline"
className={cn(
'relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50',
className
@@ -0,0 +1,56 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
// Demo ErrorCodes component — mirrors the real `<ErrorCodes service="..." />` used in
// apps/docs MDX files (e.g. apps/docs/content/guides/auth/debugging/error-codes.mdx).
interface ErrorCodesProps {
service?: string
}
const ERROR_CODES: Record<string, Array<{ code: string; description: string }>> = {
auth: [
{ code: 'invalid_credentials', description: 'Invalid email or password provided' },
{ code: 'session_not_found', description: 'User session not found or has expired' },
{ code: 'weak_password', description: 'Password does not meet strength requirements' },
{ code: 'email_not_confirmed', description: 'Email address has not been verified' },
{ code: 'mfa_required', description: 'Multi-factor authentication is required' },
],
database: [
{ code: 'connection_timeout', description: 'Database connection timeout after 30 seconds' },
{ code: 'query_failed', description: 'Query execution failed due to syntax error' },
{ code: 'permission_denied', description: 'User does not have permission for this operation' },
{ code: 'row_level_security', description: 'Row-level security policy blocked the request' },
],
realtime: [
{ code: 'SUBSCRIPTION_JOINED', description: 'Client successfully subscribed to a channel' },
{ code: 'SUBSCRIPTION_LEFT', description: 'Client left a subscribed channel' },
{ code: 'MESSAGE_BROADCAST', description: 'Broadcast message received on channel' },
{ code: 'PRESENCE_STATE', description: 'Presence state synchronized' },
],
}
export const ErrorCodes = ({ service = 'auth' }: ErrorCodesProps) => {
const errorCodes = ERROR_CODES[service] || ERROR_CODES.auth
return (
<div className="my-6 w-full overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="font-semibold">Error Code</TableHead>
<TableHead className="font-semibold">Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{errorCodes.map((item) => (
<TableRow key={item.code} className="even:bg-surface-75/75">
<TableCell>
<code className="text-sm font-mono">{item.code}</code>
</TableCell>
<TableCell>{item.description}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
@@ -111,7 +111,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -128,7 +128,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -205,7 +205,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -222,7 +222,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -268,7 +268,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -279,7 +279,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -40,7 +40,7 @@ const ThemeSwitcherDropdown = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
variant="text"
size="tiny"
className="px-1 group"
icon={
@@ -18,7 +18,7 @@ export const TopNavigation = () => {
<div className="max-w-site w-full flex flex-row items-center gap-6 mx-auto md:px-6 px-4 justify-between">
<div className="flex items-center gap-2">
<Button
type="text"
variant="text"
size="tiny"
className="px-1 md:hidden"
onClick={toggle}
+7 -2
View File
@@ -86,13 +86,18 @@ export const docsConfig: DocsConfig = {
items: [],
},
{
title: 'Markdown',
href: '/docs/ui-patterns/markdown',
items: [],
},
{
title: 'Modality',
href: '/docs/ui-patterns/modality',
title: 'Modality',
items: [],
},
{
href: '/docs/ui-patterns/navigation',
title: 'Navigation',
href: '/docs/ui-patterns/navigation',
items: [],
},
{
@@ -14,11 +14,9 @@ import { Button } from '@/components/ui/button'
```
```tsx
<Button type="outline">Button</Button>
<Button variant="outline">Button</Button>
```
_It is likely that the `type` prop will be changed to `variant` in the future._
## Link
You can use the `buttonVariants` helper to create a link that looks like a button.
@@ -47,9 +45,9 @@ Use the `size` prop to determine the size of the button.
<ComponentPreview name="button-sizes" />
### Types
### Variants
These are all the different `type` variations.
These are all the different `variant` variations.
#### Primary
@@ -61,15 +59,15 @@ Used for data insertion actions, confirming purchases, strong positive actions.
Used for opening dialogs, navigating to pages, and other non CRUD actions.
This `type` will probably be the most used button type.
It will probably be changed to be the default type in future.
This `variant` will probably be the most used button variant.
It will probably be changed to be the default variant in future.
<ComponentPreview name="button-default" />
#### Secondary
Can be used for signaling a data or config change, but not as serious as a primary button.
For destructive or side effect actions, use the `destructive` or `warning` type.
For destructive or side effect actions, use the `destructive` or `warning` variant.
<ComponentPreview name="button-secondary" />
@@ -83,7 +81,7 @@ Used for actions that might have a side effect, but not as serious as a destruct
Used for actions that will have a serious destructive side effect, like deleting data.
prop `type` will probably be changed to `destructive` in the future.
prop `variant` will probably be changed to `destructive` in the future.
<ComponentPreview name="button-destructive" />
@@ -97,7 +95,7 @@ Used for secondary actions, or actions that are not as important as the primary
Used for actions that are not as important as the primary action, or for actions that are not as important as the primary action.
prop `type` will probably be changed to `ghost` in the future.
prop `variant` will probably be changed to `ghost` in the future.
<ComponentPreview name="button-ghost" />
@@ -10,7 +10,7 @@ source:
## Installation
The Date Picker is built using a composition of the `<Popover />` and the `<Calendar />` components.
The Date Picker is built using a composition of the `<Popover />`, `<Button />` and `<Calendar />` components.
See installation instructions for the [Popover](/docs/components/popover#installation) and the [Calendar](/docs/components/calendar#installation) components.
@@ -20,41 +20,42 @@ See installation instructions for the [Popover](/docs/components/popover#install
'use client'
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { Button, Calendar } from 'ui'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
export function DatePickerDemo() {
const [date, setDate] = React.useState<Date>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton variant="outline" className="w-[280px]">
{date ? format(date, 'PPP') : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent>
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
)
}
```
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information.
## API Reference
- `<DatePicker />` accepts the same props as [the `<Popover />` component](./popover).
- `<DatePickerTrigger />` accepts the same props as [the `<PopoverTrigger />` component](./popover#api-reference).
- `<DatePickerContent />` accepts the same props as [the `<PopoverContent />` component](./popover#api-reference).
- `<DatePickerButton />` accepts the same props as [the `<Button />` component](./button) and an optional `isInvalid` prop to visually indicate an invalid date.
## Examples
### Date Picker
@@ -118,7 +118,7 @@ Use the `actions` prop to add any actions like a `<Button />`.
title="No functions found"
description="Create your first serverless function to get started."
actions={
<Button type="default" onClick={createAction}>
<Button variant="default" onClick={createAction}>
Create Function
</Button>
}
@@ -24,7 +24,7 @@ import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcr
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button type="primary" size="tiny">
<Button variant="primary" size="tiny">
Create
</Button>
</PageBreadcrumbsActions>
@@ -44,7 +44,7 @@ Compound header for page context: optional icon, title, description, and aside a
<PageHeaderDescription>Manage email templates for your project.</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<Button type="primary" size="tiny">
<Button variant="primary" size="tiny">
Create template
</Button>
</PageHeaderAside>
@@ -29,7 +29,7 @@ export default function TextConfirmDialogDemo() {
return (
<>
<Button type="danger" onClick={() => setVisible(true)}>
<Button variant="danger" onClick={() => setVisible(true)}>
Show Text Confirm Dialog
</Button>
@@ -11,6 +11,7 @@ These patterns help ensure consistency across Supabase products by establishing
- **[Empty States](empty-states)**: Communicating the absence of data and guiding users toward meaningful actions.
- **[Forms](forms)**: Building cohesive form experiences in both page layouts and side panels.
- **[Layout](layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization.
- **[Markdown](markdown)**: Rendering composable markdown content with defaults
- **[Navigation](navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts.
UI patterns may incorporate external libraries (such as `react-markdown`, `reactflow`, `recharts`, etc) or compose various components from the `ui` package. They serve as blueprints for solving recurring design problems, ensuring that similar features across the application follow the same structural and interaction patterns.
@@ -0,0 +1,101 @@
---
title: Markdown
description: Composable Markdown renderer
---
A composable `react-markdown` component with defaults for all standard markdown elements (headings, paragraphs, lists, blockquotes, links, images, tables, code). Customize any element via the `components` prop and import optional components such as Quote or Avatar.
All components are lazy-loaded.
<ComponentPreview name="markdown-full-example" peekCode />
## API
| Prop | Type | Default | Description |
| --------------- | --------------------- | ------------- | ---------------------------------------------------------------------- |
| `children` | `string` | - | Markdown content to render |
| `content` | `string` | `''` | **Deprecated**: Use `children` instead |
| `codeBlock` | `boolean` | `false` | Enable syntax highlighting for code blocks (lazy-loaded via CodeBlock) |
| `components` | `Partial<Components>` | - | Override or add specific markdown elements |
| `className` | `string` | - | CSS class for the wrapper div |
| `remarkPlugins` | `PluggableList` | `[remarkGfm]` | Additional remark plugins (GFM is included by default) |
All other `react-markdown` options are supported via spread props.
## Customization
Override any element or add new ones via the `components` prop.
<ComponentPreview name="markdown-customization" peekCode />
## Primitive Components
```tsx
import {
Anchor,
Avatar,
Blockquote,
Code,
CodeBlockPre,
DefaultPre,
Hr,
Img,
Paragraph,
Quote,
SimplePre,
Table,
Td,
Th,
Tr,
UnorderedList,
} from 'ui-patterns/Markdown'
```
### Headings
<ComponentPreview name="markdown-headings" />
### Paragraphs
<ComponentPreview name="markdown-paragraphs" />
### Lists
<ComponentPreview name="markdown-lists" />
### Links
<ComponentPreview name="markdown-links" />
### Inline Code
<ComponentPreview name="markdown-inline-code" />
### Blockquotes
<ComponentPreview name="markdown-blockquotes" />
### Code Blocks
<ComponentPreview name="markdown-code-blocks" />
### Tables
<ComponentPreview name="markdown-tables" />
### Images
<ComponentPreview name="markdown-images" />
### Horizontal Rules
<ComponentPreview name="markdown-horizontal-rules" />
## Optional Components
### Quote
<ComponentPreview name="markdown-quote-component" peekCode />
### Avatar
<ComponentPreview name="markdown-avatar-component" peekCode />
@@ -116,7 +116,7 @@ const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
<Sheet open={visible} onOpenChange={handleOpenChange}>
...
<Button type="default" onClick={confirmOnClose}>
<Button variant="default" onClick={confirmOnClose}>
Cancel
</Button>
...
@@ -0,0 +1,33 @@
import { visit } from 'unist-util-visit'
// Matches self-closing JSX-like elements with PascalCase component names:
// <ComponentName />
// <ComponentName service="auth" />
const JSX_SELF_CLOSING = /^<([A-Z]\w*)((?:\s+[\w-]+(?:="[^"]*")?)*)\s*\/>$/
const ATTR_PATTERN = /([\w-]+)(?:="([^"]*)")?/g
/**
* Remark plugin that converts JSX-like self-closing tags (e.g. `<ErrorCodes service="auth" />`)
* in markdown into nodes that react-markdown maps to entries in the `components` prop —
* giving the ui-patterns `Markdown` component MDX-like behavior in design-system demos.
*/
export const remarkJsxComponents = () => (tree: any) => {
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
if (!parent || index === undefined) return
const match = node.value.trim().match(JSX_SELF_CLOSING)
if (!match) return
const [, name, attrsStr] = match
const properties: Record<string, string | boolean> = {}
ATTR_PATTERN.lastIndex = 0
let attrMatch: RegExpExecArray | null
while ((attrMatch = ATTR_PATTERN.exec(attrsStr || '')) !== null) {
properties[attrMatch[1]] = attrMatch[2] !== undefined ? attrMatch[2] : true
}
parent.children[index] = {
type: 'jsxComponent',
data: { hName: name, hProperties: properties },
}
})
}
@@ -1,7 +1,7 @@
'use client'
import { BarChart2, ExternalLink } from 'lucide-react'
import { Badge } from 'ui'
import { Badge, CriticalIcon } from 'ui'
import {
Chart,
ChartActions,
@@ -44,6 +44,35 @@ export default function ChartComposedStates() {
</ChartCard>
</Chart>
<Chart isErrored={true}>
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={true}
errorState={
<ChartEmptyState
icon={<CriticalIcon />}
title="Some error happened"
description="Error happened why trying to fetch data. Please try again later."
/>
}
emptyState={
<ChartEmptyState
icon={<BarChart2 size={20} />}
title="No data to show"
description="It may take up to 24 hours for data to refresh"
/>
}
loadingState={<ChartLoadingState />}
>
My chart here...
</ChartContent>
</ChartCard>
</Chart>
<Chart>
<ChartCard>
<ChartHeader>
@@ -10,14 +10,14 @@ export default function AdmonitionDemo() {
title="Set up custom SMTP"
description="You’re using the built-in email service. This service has rate limits and is not meant to be
used for production apps."
actions={<Button type="default">Set up SMTP</Button>}
actions={<Button variant="default">Set up SMTP</Button>}
/>
<Admonition
type="destructive"
layout="horizontal"
title="Deleting this organization will also remove its projects"
description="Make sure you have made a backup of your projects if you want to keep your data."
actions={<Button type="danger">Delete organization</Button>}
actions={<Button variant="danger">Delete organization</Button>}
/>
</div>
)
@@ -9,7 +9,7 @@ export default function AdmonitionDemo() {
title="OAuth Server is disabled"
description="Enable OAuth Server to make your project act as an identity provider for
third-party applications."
actions={<Button type="default">OAuth Server Settings</Button>}
actions={<Button variant="default">OAuth Server Settings</Button>}
/>
)
}
@@ -8,7 +8,7 @@ export default function AdmonitionDemo() {
layout="responsive"
title="Disk management has moved"
description="Disk management is now handled alongside Project Compute on the Compute and Disk page."
actions={<Button type="default">Go to Compute and Disk</Button>}
actions={<Button variant="default">Go to Compute and Disk</Button>}
/>
)
}
@@ -36,7 +36,7 @@ export default function AlertDialogAsyncError() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -19,7 +19,7 @@ export default function AlertDialogAsync() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -14,7 +14,7 @@ export default function AlertDialogCloseOnly() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogDemo() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogDestructive() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="danger">Show Alert Dialog</Button>
<Button variant="danger">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogWarning() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="warning">Show Alert Dialog</Button>
<Button variant="warning">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="default">Button rest</Button>
<Button type="default" loading>
<Button variant="default">Button rest</Button>
<Button variant="default" loading>
Button loading
</Button>
<Button type="default" icon={<Mail />}>
<Button variant="default" icon={<Mail />}>
Button icon
</Button>
<Button type="default" iconRight={<Mail />}>
<Button variant="default" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="primary">Button rest</Button>
<Button type="primary" loading>
<Button variant="primary">Button rest</Button>
<Button variant="primary" loading>
Button loading
</Button>
<Button type="primary" icon={<Mail />}>
<Button variant="primary" icon={<Mail />}>
Button icon
</Button>
<Button type="primary" iconRight={<Mail />}>
<Button variant="primary" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDestructive() {
return (
<div className="flex gap-3">
<Button type="danger">Button rest</Button>
<Button type="danger" loading>
<Button variant="danger">Button rest</Button>
<Button variant="danger" loading>
Button loading
</Button>
<Button type="danger" icon={<Mail />}>
<Button variant="danger" icon={<Mail />}>
Button icon
</Button>
<Button type="danger" iconRight={<Mail />}>
<Button variant="danger" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonGhost() {
return (
<div className="flex gap-3">
<Button type="text">Button rest</Button>
<Button type="text" loading>
<Button variant="text">Button rest</Button>
<Button variant="text" loading>
Button loading
</Button>
<Button type="text" icon={<Mail />}>
<Button variant="text" icon={<Mail />}>
Button icon
</Button>
<Button type="text" iconRight={<Mail />}>
<Button variant="text" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -2,5 +2,5 @@ import { ChevronRight } from 'lucide-react'
import { Button } from 'ui'
export default function ButtonIcon() {
return <Button type="outline" icon={<ChevronRight className="h-4 w-4" />}></Button>
return <Button variant="outline" icon={<ChevronRight className="h-4 w-4" />}></Button>
}
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonLink() {
return (
<div className="flex gap-3">
<Button type="link">Button rest</Button>
<Button type="link" loading>
<Button variant="link">Button rest</Button>
<Button variant="link" loading>
Button loading
</Button>
<Button type="link" icon={<Mail />}>
<Button variant="link" icon={<Mail />}>
Button icon
</Button>
<Button type="link" iconRight={<Mail />}>
<Button variant="link" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonOutline() {
return (
<div className="flex gap-3">
<Button type="outline">Button rest</Button>
<Button type="outline" loading>
<Button variant="outline">Button rest</Button>
<Button variant="outline" loading>
Button loading
</Button>
<Button type="outline" icon={<Mail />}>
<Button variant="outline" icon={<Mail />}>
Button icon
</Button>
<Button type="outline" iconRight={<Mail />}>
<Button variant="outline" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonSecondary() {
return (
<div className="flex gap-3">
<Button type="secondary">Button rest</Button>
<Button type="secondary" loading>
<Button variant="secondary">Button rest</Button>
<Button variant="secondary" loading>
Button loading
</Button>
<Button type="secondary" icon={<Mail />}>
<Button variant="secondary" icon={<Mail />}>
Button icon
</Button>
<Button type="secondary" iconRight={<Mail />}>
<Button variant="secondary" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,19 +4,19 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="default" size="tiny">
<Button variant="default" size="tiny">
Tiny Button
</Button>
<Button type="default" size="small">
<Button variant="default" size="small">
Small button
</Button>
<Button type="default" size="medium">
<Button variant="default" size="medium">
Medium button
</Button>
<Button type="default" size="large">
<Button variant="default" size="large">
Large button
</Button>
<Button type="default" size="xlarge">
<Button variant="default" size="xlarge">
Huge button
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="warning">Button rest</Button>
<Button type="warning" loading>
<Button variant="warning">Button rest</Button>
<Button variant="warning" loading>
Button loading
</Button>
<Button type="warning" icon={<Mail />}>
<Button variant="warning" icon={<Mail />}>
Button icon
</Button>
<Button type="warning" iconRight={<Mail />}>
<Button variant="warning" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -57,7 +57,7 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type={'default'}
variant="default"
size="small"
className={cn(
'w-[240px] justify-start',
@@ -84,7 +84,7 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -57,7 +57,7 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type={'outline'}
variant="outline"
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground'
@@ -83,7 +83,7 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -112,7 +112,7 @@ export default function CheckboxReactHookFormMultiple() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -59,7 +59,7 @@ export default function CheckboxReactHookFormSingle() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -12,7 +12,7 @@ export default function CollapsibleDemo() {
<div className="flex items-center justify-between space-x-4 px-4">
<h4 className="text-sm font-semibold">@peduarte starred 3 repositories</h4>
<CollapsibleTrigger asChild>
<Button type="outline" size="medium" className="w-9 p-0">
<Button variant="outline" size="medium" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
@@ -48,7 +48,7 @@ export default function ComboboxDemo() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
role="combobox"
size={'small'}
aria-expanded={open}
@@ -49,7 +49,7 @@ export default function ComboboxDropdownMenu() {
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
type="text"
variant="text"
size="tiny"
icon={<MoreHorizontal className="text-foreground-muted" />}
></Button>
@@ -73,7 +73,7 @@ export default function ComboboxForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type="default"
variant="default"
role="combobox"
className={cn(
'w-[200px] justify-between',
@@ -128,7 +128,7 @@ export default function ComboboxForm() {
</FormItem>
)}
/>
<Button htmlType="submit" type="secondary" size="small">
<Button type="submit" variant="secondary" size="small">
Submit
</Button>
</form>
@@ -69,7 +69,7 @@ export default function ComboboxPopover() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
size="small"
className="w-[150px] justify-start rounded-full"
icon={
@@ -58,7 +58,7 @@ export default function ComboBoxResponsive() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
size="small"
className="w-[150px] justify-start"
icon={!selectedStatus && <Plus className="text-foreground-muted" />}
@@ -76,7 +76,7 @@ export default function ComboBoxResponsive() {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button type="outline" className="w-[150px] justify-start">
<Button variant="outline" className="w-[150px] justify-start">
{selectedStatus ? <>{selectedStatus.label}</> : <>+ Set status</>}
</Button>
</DrawerTrigger>
@@ -24,7 +24,7 @@ export default function ConfirmationModalDemo() {
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
<Button variant="default" onClick={() => setVisible(!visible)}>
Show Confirmation Modal
</Button>
<ConfirmationModal
@@ -7,15 +7,15 @@ export default function CopyButtonVerbs() {
<div className="flex flex-row gap-16">
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Bad Example</span>
<Button type="primary">Table creation</Button>
<Button type="primary">Save action</Button>
<Button type="danger">Bucket deletion</Button>
<Button variant="primary">Table creation</Button>
<Button variant="primary">Save action</Button>
<Button variant="danger">Bucket deletion</Button>
</div>
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Good Example</span>
<Button type="primary">Create table</Button>
<Button type="primary">Save changes</Button>
<Button type="danger">Delete bucket</Button>
<Button variant="primary">Create table</Button>
<Button variant="primary">Save changes</Button>
<Button variant="danger">Delete bucket</Button>
</div>
</div>
)
Loaded 100 of 2625 files, more files were not shown because too many files have changed in this diff. Show more