mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
Merge branch 'www-refresh' into docs-font-update
This commit is contained in:
2625 files changed
+151207
-950705
No files matched your search
+1
-1
@@ -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) |
|
||||
|
||||
@@ -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()`
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ updates:
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
cooldown:
|
||||
default-days: 7
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
|
||||
+12
-2
@@ -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/**'
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
packages
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
packages
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
})
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
PROD_AWS_ROLE:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
packages/pg-meta
|
||||
packages/tsconfig
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps
|
||||
blocks
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
packages
|
||||
patches
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps/www
|
||||
packages
|
||||
|
||||
@@ -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__/
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in new issue
Block a user