mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
Merge remote-tracking branch 'origin/aliwaseem/fe-3669-run-e2e-tests-against-multigres-docker-setup' into aliwaseem/fe-3669-run-e2e-tests-against-multigres-docker-setup
This commit is contained in:
commit
4c85d0a36e
2071 files changed
+88044
-51989
No files matched your search
@@ -0,0 +1,226 @@
|
||||
---
|
||||
name: clickhouse-logs-queries
|
||||
description: >-
|
||||
Write, review, and migrate Supabase logs queries against the ClickHouse-backed
|
||||
`logs` table (the `logs.all.otel` analytics endpoint). Use this whenever a task
|
||||
involves Logs Explorer SQL, the `log_attributes` map, querying a log `source`
|
||||
(edge_logs, postgres_logs, auth_logs, etc.), translating an old BigQuery
|
||||
`cross join unnest(metadata)` logs query to ClickHouse, or wiring analytics log
|
||||
SQL in `apps/studio/data/logs` and `apps/studio/components/interfaces/Settings/Logs`.
|
||||
Reach for it even when the user just says "logs query", "Logs Explorer", or
|
||||
pastes a BigQuery logs query to convert, not only when they name ClickHouse.
|
||||
---
|
||||
|
||||
# Querying Supabase logs (ClickHouse)
|
||||
|
||||
Supabase logs live in a single ClickHouse `logs` table, served by the
|
||||
`logs.all.otel` analytics endpoint. Every log line from every part of the stack
|
||||
is one row in this table, tagged by a `source` column. This replaces the older
|
||||
BigQuery model, where each service had its own table and fields were reached
|
||||
through `cross join unnest(metadata)`.
|
||||
|
||||
Two kinds of work use this skill, and they share the same SQL model:
|
||||
|
||||
1. **Writing or reviewing a logs query** (in the Logs Explorer or anywhere a raw
|
||||
ClickHouse logs query is needed). Start here in this file.
|
||||
2. **Wiring a logs query in the Studio codebase** (branded analytics SQL, the
|
||||
endpoint picker, the OTEL query builders). Read
|
||||
[references/codebase-integration.md](references/codebase-integration.md).
|
||||
|
||||
If you are converting an existing BigQuery logs query, read
|
||||
[references/bigquery-migration.md](references/bigquery-migration.md) for the full
|
||||
translation table.
|
||||
|
||||
## The logs table
|
||||
|
||||
Each row has a small set of real columns. Everything specific to a service lives
|
||||
in `log_attributes`.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ---------------- | --------------------- | ----------------------------------------------------- |
|
||||
| `id` | `String` | Unique log identifier. |
|
||||
| `timestamp` | `DateTime64` (UTC) | When the log was produced. Order/compare it directly. |
|
||||
| `event_message` | `String` | The raw log line. |
|
||||
| `severity_text` | `String` | Log level, when the source sets one. |
|
||||
| `source` | `String` | The service the log came from. Always filter on this. |
|
||||
| `log_attributes` | `Map(String, String)` | Structured per-source fields, keyed by a dotted path. |
|
||||
|
||||
`timestamp` is formatted like `2026-06-22T09:34:06.215000` (ISO 8601, microsecond
|
||||
precision, no trailing `Z`). In the Logs Explorer the selected time range is
|
||||
applied for you, so you rarely need to write a `timestamp` filter by hand.
|
||||
|
||||
A minimal, well-formed query. Lead with a comment naming the query, filter by
|
||||
`source`, and always `limit`:
|
||||
|
||||
```sql
|
||||
-- recent edge requests
|
||||
select timestamp, event_message
|
||||
from logs
|
||||
where source = 'edge_logs'
|
||||
order by timestamp desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
`source` selects the service. The common ones:
|
||||
|
||||
- `edge_logs` — API gateway requests and responses
|
||||
- `postgres_logs` — database statements and errors (also where pg_cron logs live)
|
||||
- `auth_logs` — authentication and authorization activity
|
||||
- `function_edge_logs` — edge function requests and responses
|
||||
- `function_logs` — `console` output from inside edge functions
|
||||
- `storage_logs` — object upload and retrieval activity
|
||||
- `realtime_logs` — Realtime client connections
|
||||
- `postgrest_logs`, `supavisor_logs`, `pgbouncer_logs` — mostly `id`, `timestamp`, `event_message`
|
||||
|
||||
The Logs Explorer **Field Reference** drawer lists every source and the fields it
|
||||
actually sets. When in doubt about a key, discover it from real data rather than
|
||||
guessing (see below).
|
||||
|
||||
## Reading fields from log_attributes
|
||||
|
||||
`log_attributes` maps a string key to a string value. Read a field with bracket
|
||||
access. There are no unnesting joins:
|
||||
|
||||
```sql
|
||||
select
|
||||
log_attributes['request.method'] as method,
|
||||
log_attributes['request.path'] as path,
|
||||
log_attributes['response.status_code'] as status
|
||||
from logs
|
||||
where source = 'edge_logs'
|
||||
```
|
||||
|
||||
The key keeps the dotted path that BigQuery expressed through nested structs, with
|
||||
the `metadata` root dropped: BigQuery `metadata.request.method` becomes
|
||||
`log_attributes['request.method']`. Keep the full prefix — `request.cf.country`
|
||||
is `log_attributes['request.cf.country']`, not `log_attributes['cf.country']`.
|
||||
|
||||
Common keys by source:
|
||||
|
||||
- `edge_logs`: `request.method`, `request.path`, `request.search`, `response.status_code`, `identifier`
|
||||
- `postgres_logs`: `parsed.error_severity`, `parsed.detail`, `parsed.hint`, `parsed.query`, `identifier`
|
||||
- `auth_logs`: `level`, `status`, `path`, `msg`, `error`
|
||||
- `function_edge_logs`: `response.status_code`, `request.method`, `request.pathname`, `function_id`, `execution_id`, `execution_time_ms`
|
||||
- `function_logs`: `event_type`, `function_id`, `execution_id`, `level`
|
||||
|
||||
### Numeric fields are strings
|
||||
|
||||
Map values are always strings. To compare or aggregate a numeric field, wrap it in
|
||||
`toInt32OrZero`, which returns `0` for missing or non-numeric values so it never
|
||||
errors on partial data:
|
||||
|
||||
```sql
|
||||
select count() as server_errors
|
||||
from logs
|
||||
where source = 'edge_logs'
|
||||
and toInt32OrZero(log_attributes['response.status_code']) between 500 and 599
|
||||
```
|
||||
|
||||
### Discover the keys a source sets
|
||||
|
||||
Read `mapKeys` from recent rows rather than guessing key names:
|
||||
|
||||
```sql
|
||||
select arrayJoin(mapKeys(log_attributes)) as key, count() as n
|
||||
from logs
|
||||
where source = 'postgres_logs'
|
||||
group by key
|
||||
order by n desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
`arrayJoin(mapKeys(...))` flattens the map keys into one row per key so you can
|
||||
rank them by frequency. (The Studio codebase does exactly this for the Field
|
||||
Reference drawer and to feed real keys to the AI rewrite.)
|
||||
|
||||
## ClickHouse vs BigQuery functions
|
||||
|
||||
These are the substitutions that trip people up most:
|
||||
|
||||
| Need | BigQuery | ClickHouse |
|
||||
| ------------------ | ----------------------------- | -------------------------------------------- |
|
||||
| Count rows | `count(*)` | `count()` |
|
||||
| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` |
|
||||
| Substring match | `x like '%p%'` | `x ilike '%p%'` (case-insensitive) or `like` |
|
||||
| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` |
|
||||
| Read the timestamp | `cast(timestamp as datetime)` | `timestamp` (use the column directly) |
|
||||
| Map keys | n/a (used unnest) | `mapKeys(log_attributes)` |
|
||||
|
||||
The `logs.all.otel` analytics endpoint (and the Logs Explorer on top of it)
|
||||
rejects `count(*)` and `select *` — use `count()` and list the columns you need.
|
||||
(Raw ClickHouse supports both; this is a constraint of the logs query surface.)
|
||||
|
||||
## Best practices
|
||||
|
||||
These keep queries correct and cheap. Log tables are large; an unbounded scan
|
||||
reads far more data than you need.
|
||||
|
||||
- **Start every query with an identifying comment** (e.g. `-- errors since last deploy`). It labels the query in logs and review, and makes each of several queries in a file easy to tell apart.
|
||||
- **Always include a `LIMIT`.** Even for aggregates while you iterate.
|
||||
- **Always query `from logs where source = '...'`.** There is no per-service table (no `edge_logs`, `postgres_logs`, etc. table) — there is one `logs` table, and `source` scopes it to a service. Filtering by `source` is required, not just an optimization.
|
||||
- **Keep the time range tight.** A smaller window returns results faster.
|
||||
- **Filter on the real columns** (`source`, `timestamp`) before reaching into
|
||||
`log_attributes`.
|
||||
- **Order by `timestamp desc`** to see the most recent logs first.
|
||||
- **Use `count()`**, not `count(*)` or `select *`.
|
||||
|
||||
## Worked examples
|
||||
|
||||
Requests by status code:
|
||||
|
||||
```sql
|
||||
select
|
||||
toInt32OrZero(log_attributes['response.status_code']) as status,
|
||||
count() as count
|
||||
from logs
|
||||
where source = 'edge_logs'
|
||||
group by status
|
||||
order by count desc
|
||||
limit 50
|
||||
```
|
||||
|
||||
Auth errors:
|
||||
|
||||
```sql
|
||||
select timestamp, event_message, log_attributes['msg'] as message
|
||||
from logs
|
||||
where source = 'auth_logs'
|
||||
and log_attributes['level'] in ('error', 'fatal')
|
||||
order by timestamp desc
|
||||
limit 100
|
||||
```
|
||||
|
||||
Search the raw message:
|
||||
|
||||
```sql
|
||||
select timestamp, event_message
|
||||
from logs
|
||||
where source = 'postgres_logs'
|
||||
and event_message ilike '%deadlock%'
|
||||
order by timestamp desc
|
||||
limit 100
|
||||
```
|
||||
|
||||
Postgres errors grouped by severity (the canonical unnest-to-map conversion):
|
||||
|
||||
```sql
|
||||
select log_attributes['parsed.error_severity'] as severity, count() as count
|
||||
from logs
|
||||
where source = 'postgres_logs'
|
||||
and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC')
|
||||
group by severity
|
||||
order by count desc
|
||||
limit 100
|
||||
```
|
||||
|
||||
## When the user pastes a BigQuery query
|
||||
|
||||
Convert it rather than running it as-is. The mechanical steps (drop the
|
||||
per-service table for `from logs where source = ...`, remove every
|
||||
`cross join unnest(...)`, rewrite unnest-alias columns as `log_attributes['...']`
|
||||
lookups, swap the functions above) are spelled out with a full before/after in
|
||||
[references/bigquery-migration.md](references/bigquery-migration.md). The Logs
|
||||
Explorer also has a built-in **Rewrite to ClickHouse** action that does this with
|
||||
AI; point users to it for one-off conversions in the dashboard.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Migrating a BigQuery logs query to ClickHouse
|
||||
|
||||
The old logs engine gave each service its own table and exposed structured fields
|
||||
through repeated `cross join unnest(...)` over a nested `metadata` column. The
|
||||
ClickHouse engine has one `logs` table and a flat `log_attributes` map. Conversion
|
||||
is mechanical once you internalize the mapping.
|
||||
|
||||
## The five steps
|
||||
|
||||
1. **Replace the table with `logs` plus a `source` filter.** The old table name is
|
||||
the `source` value: `from postgres_logs as t` becomes
|
||||
`from logs where source = 'postgres_logs'`. Never select from a per-service
|
||||
table name (`postgres_logs`, `edge_logs`, ...) on the ClickHouse engine.
|
||||
- Exception: pg_cron logs live under `source = 'postgres_logs'`.
|
||||
|
||||
2. **Remove every unnest join.** Delete `cross join unnest(metadata) as m`,
|
||||
`cross join unnest(m.parsed) as p`, `left join unnest(...) on true`, and so on.
|
||||
They have no equivalent — the data is already flat in the map.
|
||||
|
||||
3. **Rewrite every unnest-alias column as a map lookup.** A field taken off
|
||||
`unnest(metadata)` becomes `log_attributes['field']`. A field off a nested
|
||||
struct like `unnest(m.parsed)` becomes `log_attributes['parsed.field']`: keep
|
||||
the struct name as a dotted prefix, drop the `metadata` root and every alias.
|
||||
|
||||
4. **Wrap numeric fields in `toInt32OrZero(...)`** before comparing or aggregating
|
||||
them — map values are strings.
|
||||
|
||||
5. **Swap BigQuery functions for ClickHouse ones** (see the table below).
|
||||
|
||||
Preserve the original select list, filters, group by, order by, and limit intent
|
||||
throughout.
|
||||
|
||||
## Function substitutions
|
||||
|
||||
| Need | BigQuery | ClickHouse |
|
||||
| ---------------- | ----------------------------- | --------------------------- |
|
||||
| Count rows | `count(*)` | `count()` |
|
||||
| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` |
|
||||
| Substring match | `x like '%p%'` | `x ilike '%p%'` or `like` |
|
||||
| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` |
|
||||
| Timestamp value | `cast(timestamp as datetime)` | `timestamp` |
|
||||
| All columns | `select *` | list the columns explicitly |
|
||||
|
||||
## Full before/after
|
||||
|
||||
BigQuery:
|
||||
|
||||
```sql
|
||||
select count(t.timestamp) as count, p.error_severity
|
||||
from
|
||||
postgres_logs as t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as p
|
||||
where p.error_severity in ('ERROR', 'FATAL', 'PANIC')
|
||||
group by p.error_severity
|
||||
order by count desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
ClickHouse:
|
||||
|
||||
```sql
|
||||
select count() as count, log_attributes['parsed.error_severity'] as error_severity
|
||||
from logs
|
||||
where source = 'postgres_logs'
|
||||
and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC')
|
||||
group by log_attributes['parsed.error_severity']
|
||||
order by count desc
|
||||
limit 100
|
||||
```
|
||||
|
||||
Notice: `count(t.timestamp)` became `count()`, the two unnest joins are gone,
|
||||
`p.error_severity` became `log_attributes['parsed.error_severity']`, and the
|
||||
`from`/`where` targets the single table.
|
||||
|
||||
## Getting the keys right
|
||||
|
||||
The most common conversion mistake is dropping a dotted prefix — writing
|
||||
`log_attributes['x_real_ip']` when the real key is
|
||||
`log_attributes['request.headers.x_real_ip']`, or `log_attributes['cf.country']`
|
||||
instead of `log_attributes['request.cf.country']`. Do not invent or shorten keys.
|
||||
When unsure, discover the real keys from data:
|
||||
|
||||
```sql
|
||||
select arrayJoin(mapKeys(log_attributes)) as key, count() as n
|
||||
from logs
|
||||
where source = 'edge_logs'
|
||||
group by key
|
||||
order by n desc
|
||||
limit 200;
|
||||
```
|
||||
|
||||
Then map each old nested path to the matching key exactly as it appears.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Wiring a logs query in the Studio codebase
|
||||
|
||||
This covers writing analytics log SQL inside `apps/studio` so it is safe, routed
|
||||
to the right endpoint, and consistent with the existing OTEL builders. Read this
|
||||
when you are editing TypeScript that builds or runs a logs query, not when you are
|
||||
just writing a query in the Logs Explorer UI.
|
||||
|
||||
## Branded SQL: never concatenate user input
|
||||
|
||||
All analytics log SQL must be a `SafeLogSqlFragment`, built with the helpers in
|
||||
`apps/studio/data/logs/safe-analytics-sql.ts`. This is enforced by eslint, and the
|
||||
branding is what keeps interpolated values from becoming injection. The key
|
||||
exports:
|
||||
|
||||
- `safeSql\`...\``— a tagged template that only accepts`SafeLogSqlFragment`interpolations. Plain strings (and Postgres-branded`SafeSqlFragment`) are
|
||||
rejected at compile time, so you cannot accidentally drop a raw value in.
|
||||
- `analyticsLiteral(value)` — turns a string/number/boolean into a safely escaped
|
||||
literal fragment (single quotes and backslashes are escaped). Use it for every
|
||||
dynamic value, especially the `source`.
|
||||
- `joinSqlFragments(fragments, separator)` — joins already-safe fragments with a
|
||||
fixed structural separator (`' and '`, `', '`, etc.).
|
||||
- `keyword(value, allowed)` — resolves a value against a compile-time allow-list of
|
||||
fragments (e.g. an `AND`/`OR` operator). Returns the allow-listed fragment, never
|
||||
the raw input.
|
||||
- `quotedIdent(value)` — backtick-quotes a dotted identifier path after validating
|
||||
each segment.
|
||||
|
||||
There is intentionally no exported "raw" escape hatch. Compose with `safeSql` plus
|
||||
these helpers.
|
||||
|
||||
```ts
|
||||
import { analyticsLiteral, safeSql } from 'data/logs/safe-analytics-sql'
|
||||
|
||||
const source = 'edge_logs'
|
||||
const sql = safeSql`
|
||||
select timestamp, event_message
|
||||
from logs
|
||||
where source = ${analyticsLiteral(source)}
|
||||
order by timestamp desc
|
||||
limit 100
|
||||
`
|
||||
```
|
||||
|
||||
## Pick the endpoint and builder by flag
|
||||
|
||||
The ClickHouse path is gated by the `otelLegacyLogs` PostHog flag
|
||||
(`useFlag('otelLegacyLogs')` from `common`). Keep the BigQuery path working when
|
||||
the flag is off. Two helpers in `apps/studio/data/logs/logs-endpoint.ts` express
|
||||
the split:
|
||||
|
||||
- `logsAllEndpointUrl(useOtel)` — returns the `logs.all.otel` endpoint when
|
||||
`useOtel`, otherwise the legacy `logs.all` endpoint.
|
||||
- `pickLogsQueryBuilder(useOtel, otel, bq)` — picks between an OTEL builder and a
|
||||
BigQuery builder while preserving the type.
|
||||
|
||||
```ts
|
||||
const useOtel = useFlag('otelLegacyLogs')
|
||||
const builder = pickLogsQueryBuilder(useOtel, genDefaultQueryOtel, genDefaultQuery)
|
||||
const endpoint = logsAllEndpointUrl(useOtel)
|
||||
// include { otel: useOtel } in the React Query key so the two paths cache separately
|
||||
```
|
||||
|
||||
Run the fragment through `executeAnalyticsSql` (`apps/studio/data/logs/execute-analytics-sql.ts`)
|
||||
against that endpoint.
|
||||
|
||||
## Follow the existing OTEL builders
|
||||
|
||||
When you need a new query shape, mirror the generators in
|
||||
`apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts` rather than
|
||||
inventing a parallel style. They already encode the conventions:
|
||||
|
||||
- `genDefaultQueryOtel`, `genCountQueryOtel`, `genChartQueryOtel`,
|
||||
`genSingleLogQueryOtel` — the row/count/chart/single-log builders. They select
|
||||
the real columns plus source-specific `log_attributes[...]` lookups aliased to
|
||||
the leaf names the renderers expect.
|
||||
- `mapOtelPreviewRow`, `mapOtelSingleLogToLegacy`, `otelTimestampToMicros` — the
|
||||
JS normalization layer. `timestamp` must end up a microsecond number for the
|
||||
pagination cursor and the renderers, so reuse `otelTimestampToMicros` rather
|
||||
than parsing the ISO string yourself.
|
||||
|
||||
The map-key discovery hook (`apps/studio/data/logs/otel-log-keys-query.ts`,
|
||||
`fetchOtelLogKeys` / `useOtelLogKeysQuery`) is the canonical example of a small,
|
||||
correctly-branded OTEL query — read it before writing a new one.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Every dynamic value goes through `analyticsLiteral` (or another sanitizer),
|
||||
never string concatenation.
|
||||
- [ ] The query filters by `source` and includes a `LIMIT`.
|
||||
- [ ] Numeric `log_attributes` values are wrapped in `toInt32OrZero`.
|
||||
- [ ] The endpoint and builder are chosen with `logsAllEndpointUrl` /
|
||||
`pickLogsQueryBuilder` off `useFlag('otelLegacyLogs')`.
|
||||
- [ ] The React Query key distinguishes the OTEL and BigQuery paths.
|
||||
- [ ] `timestamp` is normalized to micros for any row consumed by the table/cursor.
|
||||
- [ ] There is a unit test asserting the generated SQL string (see
|
||||
`Logs.utils.otel.test.ts` and `safe-analytics-sql.test.ts` for the pattern).
|
||||
@@ -88,7 +88,7 @@ test.describe.configure({ mode: 'serial' })
|
||||
3. **`getByText` with exact match** - Good for unique text
|
||||
|
||||
```typescript
|
||||
page.getByText('Data API Access', { exact: true })
|
||||
page.getByText('Data API access', { exact: true })
|
||||
```
|
||||
|
||||
4. **`locator` with CSS** - Use sparingly, more fragile
|
||||
|
||||
@@ -125,3 +125,9 @@ Forms in sheets:
|
||||
|
||||
- `layout="horizontal"` for wider sheets
|
||||
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
|
||||
|
||||
## Copy
|
||||
|
||||
Source of truth: `apps/design-system/content/docs/copywriting.mdx` — sentence case, title case, proper nouns, voice and tone.
|
||||
|
||||
When changing visible copy, grep `e2e/studio/` for the old string.
|
||||
@@ -0,0 +1,71 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
# Don't inherit organization-level settings (they're tuned for other repos);
|
||||
# this config is self-contained and unset values use CodeRabbit defaults.
|
||||
inheritance: false
|
||||
|
||||
# Enrich linked issues with related code and potential solutions during review.
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: true
|
||||
|
||||
reviews:
|
||||
# Skip machine-generated / vendored files (mirrors .prettierignore). Keeps
|
||||
# reviews focused on hand-written code and preserves rate-limit budget on
|
||||
# large codegen diffs.
|
||||
path_filters:
|
||||
- '!pnpm-lock.yaml'
|
||||
- '!packages/api-types/types/**' # generated API types (api.d.ts, platform.d.ts)
|
||||
- '!supabase/functions/common/database-types.ts' # generated by `pnpm generate:types`
|
||||
- '!**/routeTree.gen.ts' # TanStack Router generated
|
||||
- '!**/__generated__/**'
|
||||
- '!apps/docs/features/docs/generated/**'
|
||||
- '!apps/www/.generated/**'
|
||||
- '!apps/design-system/__registry__/**'
|
||||
- '!apps/ui-library/__registry__/**'
|
||||
- '!apps/ui-library/public/r/**' # registry output
|
||||
- '!packages/icons/__registry__/**'
|
||||
- '!packages/icons/src/icons/**' # generated icon components
|
||||
|
||||
# Targeted, path-scoped review guidance, version-controlled alongside the code.
|
||||
path_instructions:
|
||||
- path: 'packages/common/telemetry-constants.ts'
|
||||
instructions: |
|
||||
Strictly enforce event naming: [object]_[verb] in snake_case. Only approved
|
||||
verbs: opened, clicked, submitted, created, removed, updated, retrieved,
|
||||
intended, evaluated, added, enabled, disabled, copied, exposed, failed,
|
||||
converted. Properties must be camelCase for new events (match existing
|
||||
convention when adding to existing events). Flag any usage of
|
||||
useSendEventMutation. Verify @group Events and @source JSDoc tags are
|
||||
accurate. Check that new interfaces are added to the TelemetryEvent union type.
|
||||
- path: 'apps/studio/components/**/!(*.test).tsx' # production components only, not tests
|
||||
instructions: |
|
||||
Only suggest adding PostHog event tracking (via useTrack from
|
||||
lib/telemetry/track, [object]_[verb] snake_case) when a new user-facing
|
||||
interaction is growth-relevant: e.g. first-use of a feature, onboarding steps,
|
||||
project/org creation, upgrade/billing actions, enabling or disabling a product
|
||||
feature, or any action that signals activation or retention. Do not suggest
|
||||
tracking for: passive views, page loads, UI-only state changes (e.g. expanding
|
||||
a panel, switching tabs in a settings page), developer/internal tooling
|
||||
interactions, or interactions clearly unrelated to product adoption.
|
||||
|
||||
# Applies our internal engineering skills (.claude/skills/) as CodeRabbit review
|
||||
# guidelines. The skills are the single source of truth — they are consumed
|
||||
# directly, with no copy of their content elsewhere.
|
||||
#
|
||||
# `applyTo` decouples where a guideline file lives from the code it governs.
|
||||
# Without it, CodeRabbit scopes a guideline file to its own directory and below;
|
||||
# our skills live in .claude/skills/, which contains no code, so they would never
|
||||
# reach apps/studio. `applyTo` points them at the right paths instead.
|
||||
knowledge_base:
|
||||
code_guidelines:
|
||||
filePatterns:
|
||||
# Studio code conventions — React/TS, UI patterns, composition, data fetching, errors
|
||||
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling}/SKILL.md'
|
||||
applyTo: 'apps/studio/**/*.{ts,tsx}'
|
||||
# Studio unit / component test conventions
|
||||
- files: '.claude/skills/{studio-testing,studio-mock-api-tests}/SKILL.md'
|
||||
applyTo: 'apps/studio/**/*.test.{ts,tsx}'
|
||||
# Studio end-to-end (Playwright) test conventions
|
||||
- files: '.claude/skills/studio-e2e-tests/SKILL.md'
|
||||
applyTo: 'e2e/studio/**/*.spec.ts'
|
||||
+1
-1
@@ -19,4 +19,4 @@
|
||||
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
|
||||
/apps/studio/pages/new/index.tsx @supabase/security
|
||||
|
||||
/packages/shared-data/compute-disk-limits.ts @supabase/infra
|
||||
/packages/shared-data/compute-disk-limits.ts @supabase/infra @supabase/platform
|
||||
@@ -54,6 +54,7 @@ Path-specific rules in `.github/instructions/`:
|
||||
- **Error Handling**: `studio-error-handling.instructions.md` — error classification, `ErrorMatcher` usage
|
||||
- **E2E Tests**: `studio-e2e-tests.instructions.md` — selector priority, anti-patterns (`waitForTimeout`, `force: true`)
|
||||
- **Composition Patterns**: `studio-composition-patterns.instructions.md` — avoid boolean props, use compound components
|
||||
- **UI Copy**: `studio-copy.instructions.md` → `apps/design-system/content/docs/copywriting.mdx`
|
||||
- **shadcn/Radix Components**: `studio-shadcn-components.instructions.md` — accessibility handled by primitives, do not flag
|
||||
- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shortcut registry pattern, search-input escape handler, when to flag missing coverage
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
applyTo: 'apps/studio/**'
|
||||
---
|
||||
|
||||
# Studio UI Copy
|
||||
|
||||
All comments are **advisory**.
|
||||
|
||||
**Source of truth:** `apps/design-system/content/docs/copywriting.mdx` — read it before writing or reviewing user-facing Studio strings.
|
||||
|
||||
## Agent checklist (not in the design doc)
|
||||
|
||||
- When changing visible copy, grep `e2e/studio/` and `.github/instructions/` for the old string.
|
||||
@@ -23,7 +23,7 @@ All comments are **advisory**.
|
||||
3. **`getByText` with exact match** — good for unique text
|
||||
|
||||
```typescript
|
||||
page.getByText('Data API Access', { exact: true })
|
||||
page.getByText('Data API access', { exact: true })
|
||||
```
|
||||
|
||||
4. **`locator` with CSS** — use sparingly, more fragile
|
||||
|
||||
@@ -55,6 +55,13 @@ jobs:
|
||||
working-directory: apps/docs/spec
|
||||
run: make download.tsdoc.v2
|
||||
|
||||
- name: Generate Dart reference dump
|
||||
# Dart has no upstream TypeDoc dump; its gitignored dump is generated
|
||||
# from the committed `supabase_dart_v2.yml` so the reference-content
|
||||
# snapshot test has something to walk.
|
||||
working-directory: apps/docs
|
||||
run: pnpm run codegen:references:dart
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
touch .env
|
||||
|
||||
@@ -80,10 +80,6 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Reset supabase
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
run: rm -rf supabase && pnpm exec supabase init && mkdir supabase/functions
|
||||
|
||||
# Authenticate with AWS ECR to avoid rate limiting
|
||||
- name: configure aws credentials
|
||||
if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork
|
||||
|
||||
@@ -40,5 +40,13 @@ jobs:
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Validate exports
|
||||
run: |
|
||||
pnpm --filter ui-patterns gen:exports
|
||||
if ! git diff --ignore-space-at-eol --exit-code --quiet packages/ui-patterns/package.json; then
|
||||
echo "Detected uncommitted changes after build. Run pnpm --filter ui-patterns gen:exports to fix it.";
|
||||
git diff;
|
||||
exit 1;
|
||||
fi
|
||||
- name: Run tests
|
||||
run: pnpm run test:ui-patterns
|
||||
+2
-1
@@ -112,7 +112,8 @@ next-env.d.ts
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/content-listing.code-snippets
|
||||
.idea
|
||||
.vercel
|
||||
|
||||
|
||||
@@ -21,7 +21,12 @@ apps/studio/public
|
||||
apps/**/.turbo
|
||||
apps/docs/CONTRIBUTING.md
|
||||
apps/docs/__generated__
|
||||
# Generated by apps/docs/internals/generate-markdown-manifest.ts
|
||||
apps/docs/lib/markdown-manifest.ts
|
||||
apps/design-system/__registry__
|
||||
# TanStack Router auto-generated route tree; the file header explicitly
|
||||
# says to exclude it from formatters.
|
||||
apps/studio/routeTree.gen.ts
|
||||
packages/icons/__registry__
|
||||
packages/icons/src/icons/*.ts
|
||||
apps/ui-library/__registry__
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"Content listing data export": {
|
||||
"prefix": "cl-data",
|
||||
"scope": "typescript",
|
||||
"description": "ContentListingGroup export for overview listing blocks",
|
||||
"body": [
|
||||
"export const ${1:topic}${2:Section}: ContentListingGroup = {",
|
||||
" id: '${3:topic-section}',",
|
||||
" heading: '${4:Section heading}',",
|
||||
" description: '${5:Optional intro sentence}',",
|
||||
" type: '${6|grid,list|}',",
|
||||
" items: [",
|
||||
" {",
|
||||
" title: '${7:Link title}',",
|
||||
" href: '${8:/guides/...}',",
|
||||
" description: '${9:Short description}',",
|
||||
" },",
|
||||
" ],",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Content listing inline MDX": {
|
||||
"prefix": "cl-inline",
|
||||
"scope": "markdown,mdx",
|
||||
"description": "Inline ContentListings component in a guide MDX file",
|
||||
"body": ["<ContentListings id=\"${1:topic-section}\" />"]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
apps/docs/public/.well-known/security.txt
|
||||
@@ -159,6 +159,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"admonition-button-split": {
|
||||
name: "admonition-button-split",
|
||||
type: "components:example",
|
||||
registryDependencies: ["admonition","button","dropdown-menu"],
|
||||
component: React.lazy(() => import("@/registry/default/example/admonition-button-split")),
|
||||
source: "",
|
||||
files: ["registry/default/example/admonition-button-split.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"admonition-description-only": {
|
||||
name: "admonition-description-only",
|
||||
type: "components:example",
|
||||
@@ -368,6 +379,72 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-demo": {
|
||||
name: "breadcrumb-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-separator": {
|
||||
name: "breadcrumb-separator",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-separator")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-separator.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-dropdown": {
|
||||
name: "breadcrumb-dropdown",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb","dropdown-menu"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-dropdown")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-dropdown.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-ellipsis": {
|
||||
name: "breadcrumb-ellipsis",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-ellipsis")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-ellipsis.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-link": {
|
||||
name: "breadcrumb-link",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-link")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-link.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"breadcrumb-responsive": {
|
||||
name: "breadcrumb-responsive",
|
||||
type: "components:example",
|
||||
registryDependencies: ["breadcrumb","button","drawer","dropdown-menu"],
|
||||
component: React.lazy(() => import("@/registry/default/example/breadcrumb-responsive")),
|
||||
source: "",
|
||||
files: ["registry/default/example/breadcrumb-responsive.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"button-demo": {
|
||||
name: "button-demo",
|
||||
type: "components:example",
|
||||
@@ -511,6 +588,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"button-split-dropdown": {
|
||||
name: "button-split-dropdown",
|
||||
type: "components:example",
|
||||
registryDependencies: ["button","dropdown-menu"],
|
||||
component: React.lazy(() => import("@/registry/default/example/button-split-dropdown")),
|
||||
source: "",
|
||||
files: ["registry/default/example/button-split-dropdown.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"calendar-demo": {
|
||||
name: "calendar-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -94,10 +94,10 @@ export function CodeFragment({
|
||||
)}
|
||||
>
|
||||
{showGrid && (
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
)}
|
||||
{showDottedGrid && (
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
)}
|
||||
<div className="z-10 relative">{ComponentPreview}</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function CommandMenu({ ...props }: DialogProps) {
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTitle className="sr-only">Search Design System...</DialogTitle>
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<CommandList>
|
||||
<CommandList className="max-h-[300px]">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{docsConfig.sidebarNav.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
|
||||
@@ -101,10 +101,10 @@ export function ComponentPreview({
|
||||
)}
|
||||
>
|
||||
{showGrid && (
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
)}
|
||||
{showDottedGrid && (
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
)}
|
||||
<div className="z-10 relative">{ComponentPreview}</div>
|
||||
{/* <div className="preview-grid-background"></div> */}
|
||||
@@ -146,10 +146,10 @@ export function ComponentPreview({
|
||||
})}
|
||||
>
|
||||
{showGrid && (
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
|
||||
)}
|
||||
{showDottedGrid && (
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
)}
|
||||
<div className="z-10 relative">{ComponentPreview}</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { RadioGroup, RadioGroupLargeItem, singleThemes, Theme } from 'ui'
|
||||
import { RadioGroup, RadioGroupLargeItem, singleThemes } from 'ui'
|
||||
|
||||
const ThemeSettings = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
@@ -33,7 +33,7 @@ const ThemeSettings = () => {
|
||||
value={theme}
|
||||
className="flex flex-wrap gap-3"
|
||||
>
|
||||
{singleThemes.map((theme: Theme) => (
|
||||
{singleThemes.map((theme) => (
|
||||
<RadioGroupLargeItem key={theme.value} value={theme.value} label={theme.name}>
|
||||
<SVG src={`${process.env.NEXT_PUBLIC_BASE_PATH}/img/themes/${theme.value}.svg`} />
|
||||
</RadioGroupLargeItem>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
singleThemes,
|
||||
Theme,
|
||||
} from 'ui'
|
||||
|
||||
const ThemeSwitcherDropdown = () => {
|
||||
@@ -59,7 +58,7 @@ const ThemeSwitcherDropdown = () => {
|
||||
value={theme}
|
||||
onValueChange={(themeValue) => setTheme(themeValue)}
|
||||
>
|
||||
{singleThemes.map((theme: Theme) => (
|
||||
{singleThemes.map((theme) => (
|
||||
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
|
||||
{theme.name}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
@@ -105,7 +105,7 @@ Used for actions that are not as important as the primary action, or for actions
|
||||
|
||||
<ComponentPreview name="button-link" />
|
||||
|
||||
### Only an Icon
|
||||
### Only an icon
|
||||
|
||||
Displaying only an Icon in a button.
|
||||
|
||||
@@ -115,12 +115,28 @@ Displaying only an Icon in a button.
|
||||
|
||||
<ComponentPreview name="button-icon" />
|
||||
|
||||
### As Child
|
||||
### As child
|
||||
|
||||
Supports slot behavior with `asChild` prop.
|
||||
|
||||
<ComponentPreview name="button-as-child" />
|
||||
|
||||
### Split with dropdown
|
||||
|
||||
Pair a button with a chevron `DropdownMenu` trigger when there are variations of the same action, or alternative ways to accomplish the same goal. The default or most likely option should be used on the exposed button.
|
||||
|
||||
When secondary actions are related but distinct—not alternatives to the primary action—display the primary action as a button and place the rest in an overflow menu instead. See [Table multiple actions](./table#multiple-actions).
|
||||
|
||||
<ComponentPreview name="button-split-dropdown" peekCode />
|
||||
|
||||
The shared middle border is the tricky part. Do **not** use `border-l-0` on the chevron button — that drops the divider on hover/focus. Instead:
|
||||
|
||||
- Primary: `rounded-r-none` and `hover:z-10` so its border stacks above the chevron on hover.
|
||||
- Chevron trigger: `rounded-l-none`, `shrink-0`, `px-[4px] py-[5px]`, and `-ml-px` to overlap the adjacent border by one pixel.
|
||||
- Chevron trigger only: `aria-label` describing the menu (the icon is decorative).
|
||||
|
||||
Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions, also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary when `layout="responsive"`.
|
||||
|
||||
## Accessibility
|
||||
|
||||
[Keyboard focus](../accessibility#focus-management) is automatically handled:
|
||||
|
||||
@@ -46,6 +46,18 @@ Style the color of the button based upon the _context_ of the Admonition, not it
|
||||
|
||||
Only ever use the `primary` (green) button `type` on a `default` Admonition. Even then—given that Admonition is an isolated callout—the `primary` button `type` should rarely be used.
|
||||
|
||||
### Split button with dropdown
|
||||
|
||||
When a callout offers alternative ways to accomplish the same goal, use a [split button](../components/button#split-with-dropdown) instead of crowding the callout with multiple buttons. For related but distinct actions, use an overflow menu. See [Table multiple actions](../components/table#multiple-actions).
|
||||
|
||||
<ComponentPreview
|
||||
name="admonition-button-split"
|
||||
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
|
||||
peekCode
|
||||
/>
|
||||
|
||||
In responsive Admonitions, wrap the pair in `flex w-full @lg:w-auto`, give the primary `flex-1 @lg:flex-none`, and keep the chevron `shrink-0`.
|
||||
|
||||
## Examples
|
||||
|
||||
<ComponentPreview
|
||||
@@ -53,7 +65,7 @@ Only ever use the `primary` (green) button `type` on a `default` Admonition. Eve
|
||||
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
|
||||
/>
|
||||
|
||||
## Responsive
|
||||
### Responsive
|
||||
|
||||
Resize your browser to see the button(s) change `layout` based on the Admonition’s width.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-table": "catalog:",
|
||||
"contentlayer2": "0.4.6",
|
||||
"common": "workspace:*",
|
||||
"date-fns": "^2.30.0",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
export default function AdmonitionButtonSplitDemo() {
|
||||
return (
|
||||
<Admonition
|
||||
type="default"
|
||||
layout="responsive"
|
||||
title="Set up custom SMTP to edit templates"
|
||||
description="Emails will be sent using the default templates. Set up custom SMTP to edit their subject and body."
|
||||
actions={
|
||||
<div className="flex w-full @lg:w-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10"
|
||||
>
|
||||
Set up SMTP
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
aria-label="More email template editing options"
|
||||
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
|
||||
icon={<ChevronDown />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuItem>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="block text-foreground">Upgrade to Pro</p>
|
||||
<p className="block text-foreground-lighter text-balance">
|
||||
Customize templates while using Supabase’s email service
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="block text-foreground">Configure Send Email hook</p>
|
||||
<p className="block text-foreground-lighter text-balance">
|
||||
Send auth emails through your own workflow
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from 'ui'
|
||||
|
||||
export default function BreadcrumbDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChevronDown, Slash } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
export default function BreadcrumbDropdownDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator>
|
||||
<Slash />
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5">
|
||||
Components
|
||||
<ChevronDown />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Documentation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Themes</DropdownMenuItem>
|
||||
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator>
|
||||
<Slash />
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from 'ui'
|
||||
|
||||
export default function BreadcrumbEllipsisDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbEllipsis />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/docs/components">Components</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from 'ui'
|
||||
|
||||
export default function BreadcrumbLinkDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/components">Components</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
function useMediaQuery(query: string) {
|
||||
const [value, setValue] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
function onChange(event: MediaQueryListEvent) {
|
||||
setValue(event.matches)
|
||||
}
|
||||
|
||||
const result = matchMedia(query)
|
||||
result.addEventListener('change', onChange)
|
||||
setValue(result.matches)
|
||||
|
||||
return () => result.removeEventListener('change', onChange)
|
||||
}, [query])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ href: '#', label: 'Home' },
|
||||
{ href: '#', label: 'Documentation' },
|
||||
{ href: '#', label: 'Build Your Application' },
|
||||
{ href: '#', label: 'Data Fetching' },
|
||||
{ label: 'Caching and Revalidating' },
|
||||
]
|
||||
|
||||
const ITEMS_TO_DISPLAY = 3
|
||||
|
||||
export default function BreadcrumbResponsiveDemo() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={items[0].href ?? '/'}>{items[0].label}</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
{items.length > ITEMS_TO_DISPLAY ? (
|
||||
<>
|
||||
<BreadcrumbItem>
|
||||
{isDesktop ? (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1" aria-label="Toggle menu">
|
||||
<BreadcrumbEllipsis className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{items.slice(1, -2).map((item, index) => (
|
||||
<DropdownMenuItem key={index} asChild>
|
||||
<Link href={item.href ? item.href : '#'}>{item.label}</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger aria-label="Toggle Menu">
|
||||
<BreadcrumbEllipsis className="h-4 w-4" />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="text-left">
|
||||
<DrawerTitle>Navigate to</DrawerTitle>
|
||||
<DrawerDescription>Select a page to navigate to.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="grid gap-1 px-4">
|
||||
{items.slice(1, -2).map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href ? item.href : '#'}
|
||||
className="py-1 text-sm"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<DrawerFooter className="pt-4">
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
</>
|
||||
) : null}
|
||||
{items.slice(-ITEMS_TO_DISPLAY + 1).map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 ? <BreadcrumbSeparator /> : null}
|
||||
<BreadcrumbItem>
|
||||
{item.href ? (
|
||||
<BreadcrumbLink asChild className="max-w-20 truncate md:max-w-none">
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPage className="max-w-20 truncate md:max-w-none">
|
||||
{item.label}
|
||||
</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Slash } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from 'ui'
|
||||
|
||||
export default function BreadcrumbSeparatorDemo() {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/">Home</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator>
|
||||
<Slash />
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/components">Components</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator>
|
||||
<Slash />
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
export default function ButtonSplitDropdownDemo() {
|
||||
return (
|
||||
<div className="flex w-fit">
|
||||
<Button type="button" variant="default" className="rounded-r-none hover:z-10">
|
||||
Primary action
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
aria-label="More actions"
|
||||
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
|
||||
icon={<ChevronDown />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem>Secondary action</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
||||
Destructive action
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Key } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
|
||||
|
||||
export default function CopyEmptyStates() {
|
||||
return (
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function DrawerDemo() {
|
||||
dataKey="goal"
|
||||
style={
|
||||
{
|
||||
fill: 'hsl(var(--foreground-default))',
|
||||
fill: 'var(--foreground-default)',
|
||||
opacity: 0.9,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionAside,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
|
||||
|
||||
export default function EmptyStatePresentationalIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BucketPlus } from 'icons'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
|
||||
|
||||
export default function EmptyStatePresentationalIcon() {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { format } from 'date-fns'
|
||||
import { useState } from 'react'
|
||||
import { DateRange } from 'react-day-picker'
|
||||
import { Button, Calendar } from 'ui'
|
||||
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns'
|
||||
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns/FilterBar'
|
||||
|
||||
function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
const [date, setDate] = useState<DateRange | undefined>(
|
||||
|
||||
@@ -150,7 +150,7 @@ const buildMockChartData = (intervalKey: string) => {
|
||||
const EXECUTION_TIME_CHART_CONFIG = {
|
||||
avg_execution_time: {
|
||||
label: 'Average Execution Time',
|
||||
color: 'hsl(var(--foreground-default))',
|
||||
color: 'var(--foreground-default)',
|
||||
},
|
||||
max_execution_time: {
|
||||
label: 'Max Execution Time',
|
||||
@@ -485,7 +485,7 @@ function OverviewPage() {
|
||||
{
|
||||
y: averageExecutionTime,
|
||||
label: 'average',
|
||||
stroke: 'hsl(var(--foreground-default))',
|
||||
stroke: 'var(--foreground-default)',
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
]}
|
||||
@@ -538,7 +538,7 @@ function OverviewPage() {
|
||||
{
|
||||
y: averageCpuTime,
|
||||
label: 'average',
|
||||
stroke: 'hsl(var(--foreground-default))',
|
||||
stroke: 'var(--foreground-default)',
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
]}
|
||||
@@ -589,7 +589,7 @@ function OverviewPage() {
|
||||
{
|
||||
y: averageMemoryUsage,
|
||||
label: 'average',
|
||||
stroke: 'hsl(var(--foreground-default))',
|
||||
stroke: 'var(--foreground-default)',
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
import { FilterBar, isGroup, type FilterCondition, type FilterGroup } from 'ui-patterns'
|
||||
import { FilterBar, isGroup, type FilterCondition, type FilterGroup } from 'ui-patterns/FilterBar'
|
||||
|
||||
const logs = [
|
||||
['10:42:01.129', 'POST /hello-world', '200', '132ms', 'iad1'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StatusCode } from 'ui-patterns'
|
||||
import { StatusCode } from 'ui-patterns/StatusCode'
|
||||
|
||||
export default function StatusCodeDemo() {
|
||||
return (
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function SwitchForm() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" variant="alternative">
|
||||
<Button type="submit" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function TextareaForm() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" variant="alternative">
|
||||
<Button type="submit" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
import { AnchorProvider, Toc, TOCItems, TOCScrollArea, type AnchorProviderProps } from 'ui-patterns'
|
||||
import {
|
||||
AnchorProvider,
|
||||
Toc,
|
||||
TOCItems,
|
||||
TOCScrollArea,
|
||||
type AnchorProviderProps,
|
||||
} from 'ui-patterns/Toc'
|
||||
|
||||
export default function MultiSelectDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
import { AnchorProvider, Toc, TOCItems, TOCScrollArea, type AnchorProviderProps } from 'ui-patterns'
|
||||
import {
|
||||
AnchorProvider,
|
||||
Toc,
|
||||
TOCItems,
|
||||
TOCScrollArea,
|
||||
type AnchorProviderProps,
|
||||
} from 'ui-patterns/Toc'
|
||||
|
||||
export default function MultiSelectDemo() {
|
||||
return (
|
||||
|
||||
@@ -25,6 +25,12 @@ export const examples: Registry = [
|
||||
registryDependencies: ['admonition'],
|
||||
files: ['example/admonition-button.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'admonition-button-split',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['admonition', 'button', 'dropdown-menu'],
|
||||
files: ['example/admonition-button-split.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'admonition-description-only',
|
||||
type: 'components:example',
|
||||
@@ -139,42 +145,42 @@ export const examples: Registry = [
|
||||
registryDependencies: ['badge'],
|
||||
files: ['example/badge-secondary.tsx'],
|
||||
},
|
||||
// {
|
||||
// name: 'breadcrumb-demo',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-demo.tsx'],
|
||||
// },
|
||||
// {
|
||||
// name: 'breadcrumb-separator',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-separator.tsx'],
|
||||
// },
|
||||
// {
|
||||
// name: 'breadcrumb-dropdown',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-dropdown.tsx'],
|
||||
// },
|
||||
// {
|
||||
// name: 'breadcrumb-ellipsis',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-ellipsis.tsx'],
|
||||
// },
|
||||
// {
|
||||
// name: 'breadcrumb-link',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-link.tsx'],
|
||||
// },
|
||||
// {
|
||||
// name: 'breadcrumb-responsive',
|
||||
// type: 'components:example',
|
||||
// registryDependencies: ['breadcrumb'],
|
||||
// files: ['example/breadcrumb-responsive.tsx'],
|
||||
// },
|
||||
{
|
||||
name: 'breadcrumb-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb'],
|
||||
files: ['example/breadcrumb-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'breadcrumb-separator',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb'],
|
||||
files: ['example/breadcrumb-separator.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'breadcrumb-dropdown',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb', 'dropdown-menu'],
|
||||
files: ['example/breadcrumb-dropdown.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'breadcrumb-ellipsis',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb'],
|
||||
files: ['example/breadcrumb-ellipsis.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'breadcrumb-link',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb'],
|
||||
files: ['example/breadcrumb-link.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'breadcrumb-responsive',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['breadcrumb', 'button', 'drawer', 'dropdown-menu'],
|
||||
files: ['example/breadcrumb-responsive.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'button-demo',
|
||||
type: 'components:example',
|
||||
@@ -253,6 +259,12 @@ export const examples: Registry = [
|
||||
registryDependencies: ['button'],
|
||||
files: ['example/button-as-child.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'button-split-dropdown',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['button', 'dropdown-menu'],
|
||||
files: ['example/button-split-dropdown.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'calendar-demo',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
[data-theme='light'],
|
||||
.light {
|
||||
--code-token-keyword: #6b35dc;
|
||||
--code-foreground: hsl(var(--foreground-light) / 1);
|
||||
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-constant: #15593b;
|
||||
--code-token-string: #f1a10d;
|
||||
--code-token-comment: #7e7e7e;
|
||||
--code-token-parameter: hsl(var(--foreground-light) / 1);
|
||||
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-function: #15593b;
|
||||
--code-token-string-expression: #f1a10d;
|
||||
--code-token-punctuation: hsl(var(--foreground-light) / 1);
|
||||
--code-token-link: hsl(var(--foreground-light) / 1);
|
||||
--code-token-number: hsl(var(--foreground-light) / 1);
|
||||
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
|
||||
--code-token-property: #15593b;
|
||||
--code-highlight-color: #1c1c1c;
|
||||
}
|
||||
@@ -197,15 +197,42 @@ Keep code lines short to avoid scrolling. For example, you can split long shell
|
||||
|
||||
Optionally specify a filename for the codeblock by including it after the opening backticks and language specifier:
|
||||
|
||||
```md
|
||||
````md
|
||||
```ts environment.ts
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
Optionally highlight lines by using `mark=${lineNumber}`.
|
||||
|
||||
```md
|
||||
````md
|
||||
```js mark=12:13
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
### Content listings
|
||||
|
||||
Overview and index pages use a single `<ContentListings id="..." />` component for curated link sections such as "Get started", "Next steps", "Examples", or "Resources". Refer to [`storage.data.ts`](data/content-listings/storage.data.ts) and [`storage.mdx`](content/guides/storage.mdx) for a full example.
|
||||
|
||||
**Prompt to add content listings:**
|
||||
|
||||
```text
|
||||
Add a content listing block for [TOPIC] / [SECTION] (for example, Storage / Examples).
|
||||
Follow CONTRIBUTING § Content listings in apps/docs.
|
||||
Copy structure from `storageGetStarted` in apps/docs/data/content-listings/storage.data.ts.
|
||||
Pick a globally-unique kebab-case id like `[topic]-[section]`.
|
||||
Run `pnpm test:local lib/content-listings.test.ts` from apps/docs.
|
||||
```
|
||||
|
||||
**Manually add content listings:**
|
||||
|
||||
1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups (e.g. `storage-get-started`, not just `get-started`) — it is used both as the lookup key and as the telemetry `listingId`.
|
||||
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level.
|
||||
3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`.
|
||||
|
||||
Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets): `cl-data` (data export with namespaced id) and `cl-inline` (MDX component).
|
||||
|
||||
|
||||
### Footnotes
|
||||
|
||||
@@ -284,7 +311,7 @@ Don't nest lists more than two deep.
|
||||
3. List item
|
||||
- List item
|
||||
- List item
|
||||
<!-- DON'T ADD ANOTHER LEVEL OF NESTING -->
|
||||
<!-- DON'T ADD ANOTHER LEVEL OF NESTING -->
|
||||
- Overly nested list item
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import { Heading } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
|
||||
@@ -24,7 +24,7 @@ import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import emoji from 'remark-emoji'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
@@ -223,6 +223,14 @@ const pageMap = [
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'mongodb',
|
||||
meta: {
|
||||
title: 'MongoDB',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'mongodb.md',
|
||||
},
|
||||
{
|
||||
slug: 'mssql',
|
||||
meta: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { GlassPanel } from 'ui-patterns'
|
||||
import { GlassPanel } from 'ui-patterns/GlassPanel'
|
||||
|
||||
import { getAiPrompts } from './AiPrompts.utils'
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'ui-patterns/ShimmeringLoader/index.css'
|
||||
import '../styles/globals.css'
|
||||
import '../styles/prism-okaidia.css'
|
||||
|
||||
import { SkipToContent } from '~/components/SkipToContent'
|
||||
import { GlobalProviders } from '~/features/app.providers'
|
||||
import { TopNavSkeleton } from '~/layouts/MainSkeleton'
|
||||
import { BASE_PATH, IS_PRODUCTION } from '~/lib/constants'
|
||||
@@ -12,6 +13,8 @@ import { TelemetryTagManager } from 'common'
|
||||
import { genFaviconData } from 'common/MetaFavicons/app-router'
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
|
||||
import { inter, manrope } from '@/fonts'
|
||||
|
||||
const { metadataApplicationName, metadataTitle } = getCustomContent([
|
||||
'metadata:application_name',
|
||||
'metadata:title',
|
||||
@@ -50,8 +53,9 @@ const viewport: Viewport = {
|
||||
|
||||
const RootLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang="en" className={`${manrope.variable} ${inter.variable}`} suppressHydrationWarning>
|
||||
<body>
|
||||
<SkipToContent />
|
||||
<TelemetryTagManager />
|
||||
<GlobalProviders>
|
||||
<TopNavSkeleton>{children}</TopNavSkeleton>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { ContentListingGroup, ContentListingItem } from '~/lib/content-listings.schema'
|
||||
import {
|
||||
getContentListingById,
|
||||
getContentListingGroupLabel,
|
||||
isExternalContentListingHref,
|
||||
} from '~/lib/content-listings.utils'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import Link from 'next/link'
|
||||
import { useCallback } from 'react'
|
||||
import { GlassPanel } from 'ui-patterns/GlassPanel'
|
||||
import { Heading } from 'ui/src/components/CustomHTMLElements'
|
||||
|
||||
const GRID_ITEM_CLASS = {
|
||||
2: 'col-span-12 md:col-span-6',
|
||||
3: 'col-span-12 md:col-span-4',
|
||||
4: 'col-span-12 md:col-span-3',
|
||||
} as const
|
||||
|
||||
function useContentListingClickHandler(group: ContentListingGroup) {
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
const groupLabel = getContentListingGroupLabel(group)
|
||||
|
||||
const trackClick = useCallback(
|
||||
(item: ContentListingItem) => {
|
||||
sendTelemetryEvent({
|
||||
action: 'docs_content_listing_clicked',
|
||||
properties: {
|
||||
targetPath: item.href,
|
||||
linkTitle: item.title,
|
||||
...(groupLabel ? { groupTitle: groupLabel } : {}),
|
||||
listingId: group.id,
|
||||
},
|
||||
})
|
||||
},
|
||||
[sendTelemetryEvent, group.id, groupLabel]
|
||||
)
|
||||
|
||||
return { trackClick }
|
||||
}
|
||||
|
||||
function ContentListingGroupHeading({ group }: { group: ContentListingGroup }) {
|
||||
if (!group.heading) return null
|
||||
|
||||
return <Heading tag={group.headingLevel ?? 'h2'}>{group.heading}</Heading>
|
||||
}
|
||||
|
||||
function ContentListingsGroup({ group }: { group: ContentListingGroup }) {
|
||||
const { trackClick } = useContentListingClickHandler(group)
|
||||
const isGrid = group.type === 'grid'
|
||||
const listClassName = isGrid ? 'grid md:grid-cols-12 gap-4' : 'list-disc pl-6 space-y-2'
|
||||
const gridItemClassName = isGrid ? GRID_ITEM_CLASS[group.columns ?? 3] : undefined
|
||||
|
||||
// Heading stays outside `not-prose` so it inherits the surrounding MDX prose
|
||||
// typography. The list itself opts out so its explicit Tailwind layout wins.
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<ContentListingGroupHeading group={group} />
|
||||
<div className="not-prose space-y-4">
|
||||
{group.description && <p className="text-foreground-light">{group.description}</p>}
|
||||
<ul className={listClassName}>
|
||||
{group.items.map((item) => {
|
||||
const external = isExternalContentListingHref(item.href)
|
||||
const key = `${group.id}-${item.href}`
|
||||
|
||||
if (isGrid) {
|
||||
return (
|
||||
<li key={key} className={gridItemClassName}>
|
||||
<Link
|
||||
href={item.href}
|
||||
passHref
|
||||
className="block h-full"
|
||||
onClick={() => trackClick(item)}
|
||||
target={external ? '_blank' : undefined}
|
||||
>
|
||||
<GlassPanel
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
hasLightIcon={Boolean(item.icon)}
|
||||
>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={key}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => trackClick(item)}
|
||||
target={external ? '_blank' : undefined}
|
||||
>
|
||||
<strong>{item.title}</strong>: {item.description}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContentListings({ id }: { id: string }) {
|
||||
const group = getContentListingById(id)
|
||||
if (!group || !group.items.length) return null
|
||||
|
||||
return (
|
||||
<div className="my-10 space-y-10">
|
||||
<ContentListingsGroup group={group} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentListings } from './ContentListings.client'
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { domAnimation, LazyMotion, m } from 'framer-motion'
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
import { LazyMotion, domAnimation, m } from 'framer-motion'
|
||||
|
||||
const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
const pathMotionConfig = {
|
||||
@@ -47,7 +47,7 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
animate={pathMotionConfig.animate}
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M125.25 191.383C167.828 191.383 202.344 156.867 202.344 114.289C202.344 71.7115 167.828 37.1953 125.25 37.1953C82.6724 37.1953 48.1562 71.7115 48.1562 114.289C48.1562 156.867 82.6724 191.383 125.25 191.383Z"
|
||||
stroke="hsl(var(--foreground-light))"
|
||||
stroke="var(--foreground-light)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
@@ -215,7 +215,7 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
animate={pathMotionConfig.animate}
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M218.024 169.846H32.498"
|
||||
stroke="hsl(var(--foreground-light))"
|
||||
stroke="var(--foreground-light)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
@@ -291,8 +291,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.451 114.82) rotate(90) scale(109.781 0.5)"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_0_1"
|
||||
@@ -302,8 +302,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="11.9994"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_0_1"
|
||||
@@ -313,8 +313,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="204.984"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_0_1"
|
||||
@@ -324,8 +324,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="13.9993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_0_1"
|
||||
@@ -335,8 +335,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="204.984"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_0_1"
|
||||
@@ -346,9 +346,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="-7.94661"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_0_1"
|
||||
@@ -358,9 +358,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="-3.99932"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_0_1"
|
||||
@@ -370,9 +370,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="199.061"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint8_radial_0_1"
|
||||
@@ -382,8 +382,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.752 114.291) rotate(90) scale(109.271 0.5)"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_0_1"
|
||||
@@ -393,8 +393,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="6.31757"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_0_1"
|
||||
@@ -404,8 +404,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="200"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_0_1"
|
||||
@@ -415,9 +415,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="199.061"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint12_radial_0_1"
|
||||
@@ -427,8 +427,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.261 59.2344) rotate(90) scale(0.5 263.749)"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0" />
|
||||
<stop stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint13_linear_0_1"
|
||||
@@ -438,9 +438,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="94.9749"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.5" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint14_linear_0_1"
|
||||
@@ -450,9 +450,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="131.43"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.5" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint15_linear_0_1"
|
||||
@@ -462,9 +462,9 @@ const DocsCoverLogo = (props: PropsWithChildren) => {
|
||||
y2="1.99785"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="hsl(var(--foreground-light))" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-lighter))" stopOpacity="0.1" />
|
||||
<stop stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--foreground-light)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--foreground-lighter)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint16_linear_0_1"
|
||||
|
||||
@@ -6,13 +6,13 @@ interface MermaidProps {
|
||||
|
||||
export function Mermaid({ chart }: MermaidProps) {
|
||||
const svg = renderMermaidSVG(chart, {
|
||||
bg: 'hsl(var(--background-default))',
|
||||
fg: 'hsl(var(--foreground-default))',
|
||||
bg: 'var(--background-default)',
|
||||
fg: 'var(--foreground-default)',
|
||||
accent: 'hsl(var(--brand-default))',
|
||||
muted: 'hsl(var(--foreground-light))',
|
||||
line: 'hsl(var(--border-strong))',
|
||||
border: 'hsl(var(--border-strong))',
|
||||
surface: 'hsl(var(--background-surface-200))',
|
||||
muted: 'var(--foreground-light)',
|
||||
line: 'var(--border-strong)',
|
||||
border: 'var(--border-strong)',
|
||||
surface: 'var(--background-surface-200)',
|
||||
transparent: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const useActiveMenuLabel = (menu: typeof GLOBAL_MENU_ITEMS) => {
|
||||
const GlobalNavigationMenu: FC = () => {
|
||||
const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS)
|
||||
const triggerClassName =
|
||||
'h-(--header-height) p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:text-foreground data-open:text-foreground! data-radix-collection-item:focus-visible:ring-2 data-radix-collection-item:focus-visible:ring-foreground-lighter data-radix-collection-item:focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none! outline-hidden transition-all outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600'
|
||||
'h-(--header-height) p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:bg-transparent hover:text-foreground data-open:bg-transparent! data-open:text-foreground! data-radix-collection-item:focus-visible:ring-2 data-radix-collection-item:focus-visible:ring-foreground-lighter data-radix-collection-item:focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none! outline-hidden transition-all outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600'
|
||||
|
||||
return (
|
||||
<div className="flex relative gap-2 justify-start items-end w-full h-full">
|
||||
|
||||
@@ -1179,12 +1179,12 @@ export const database: NavMenuConstant = {
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/replication' },
|
||||
{
|
||||
name: 'External replication',
|
||||
url: '/guides/database/replication/external-replication-setup' as `/${string}`,
|
||||
name: 'Pipelines',
|
||||
url: '/guides/database/replication/pipelines' as `/${string}`,
|
||||
items: [
|
||||
{
|
||||
name: 'Setting up',
|
||||
url: '/guides/database/replication/external-replication-setup' as `/${string}`,
|
||||
url: '/guides/database/replication/pipelines' as `/${string}`,
|
||||
items: [
|
||||
{
|
||||
name: 'BigQuery',
|
||||
@@ -1194,9 +1194,9 @@ export const database: NavMenuConstant = {
|
||||
},
|
||||
{
|
||||
name: 'Monitoring',
|
||||
url: '/guides/database/replication/external-replication-monitoring' as `/${string}`,
|
||||
url: '/guides/database/replication/pipelines-monitoring' as `/${string}`,
|
||||
},
|
||||
{ name: 'FAQ', url: '/guides/database/replication/external-replication-faq' },
|
||||
{ name: 'FAQ', url: '/guides/database/replication/pipelines-faq' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1420,6 +1420,10 @@ export const database: NavMenuConstant = {
|
||||
name: 'Logflare',
|
||||
url: '/guides/database/extensions/wrappers/logflare' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'MongoDB',
|
||||
url: '/guides/database/extensions/wrappers/mongodb' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'MSSQL',
|
||||
url: '/guides/database/extensions/wrappers/mssql' as `/${string}`,
|
||||
@@ -2096,6 +2100,7 @@ export const storage: NavMenuConstant = {
|
||||
items: [
|
||||
{ name: 'Fundamentals', url: '/guides/storage/cdn/fundamentals' },
|
||||
{ name: 'Smart CDN', url: '/guides/storage/cdn/smart-cdn' },
|
||||
{ name: 'Purging Cache', url: '/guides/storage/cdn/purge-cdn-cache' },
|
||||
{ name: 'Metrics', url: '/guides/storage/cdn/metrics' },
|
||||
],
|
||||
},
|
||||
@@ -2715,6 +2720,10 @@ export const platform: NavMenuConstant = {
|
||||
name: 'Restore to a new project',
|
||||
url: '/guides/platform/clone-project',
|
||||
},
|
||||
{
|
||||
name: 'Project Pausing',
|
||||
url: '/guides/platform/free-project-pausing' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Single Sign-On',
|
||||
url: '/guides/platform/sso',
|
||||
|
||||
@@ -10,7 +10,8 @@ import Link from 'next/link'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useState } from 'react'
|
||||
import { Button, buttonVariants, cn } from 'ui'
|
||||
import { AuthenticatedDropdownMenu, CommandMenuTriggerInput } from 'ui-patterns'
|
||||
import { AuthenticatedDropdownMenu } from 'ui-patterns/AuthenticatedDropdownMenu'
|
||||
import { CommandMenuTriggerInput } from 'ui-patterns/CommandMenu'
|
||||
|
||||
import { getCustomContent } from '../../../lib/custom-content/getCustomContent'
|
||||
import GlobalNavigationMenu from './GlobalNavigationMenu'
|
||||
@@ -47,6 +48,7 @@ const TopNavBar: FC = () => {
|
||||
<div className="flex gap-2 items-center">
|
||||
<DevToolbarTrigger />
|
||||
<CommandMenuTriggerInput
|
||||
className="[&>div>p]:text-foreground-lighter"
|
||||
placeholder={
|
||||
<>
|
||||
Search
|
||||
@@ -96,13 +98,7 @@ const HeaderLogo = memo(() => {
|
||||
const { navigationLogo } = getCustomContent(['navigation:logo'])
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'default' }),
|
||||
'flex shrink-0 items-center w-fit bg-transparent! border-none! shadow-none!'
|
||||
)}
|
||||
>
|
||||
<Link href="/" className="flex shrink-0 items-center gap-1.5 w-fit">
|
||||
<Image
|
||||
className={cn('hidden dark:block m-0!', largeLogo && 'h-[36px]')}
|
||||
src={navigationLogo?.dark ?? '/docs/supabase-dark.svg'}
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Theme,
|
||||
themes,
|
||||
singleThemes,
|
||||
} from 'ui'
|
||||
|
||||
import MenuIconPicker from './MenuIconPicker'
|
||||
@@ -106,9 +105,9 @@ const TopNavDropdown = () => {
|
||||
setTheme(value)
|
||||
}}
|
||||
>
|
||||
{themes
|
||||
{singleThemes
|
||||
.filter((x) => x.value === 'light' || x.value === 'dark' || x.value === 'system')
|
||||
.map((theme: Theme) => (
|
||||
.map((theme) => (
|
||||
<DropdownMenuRadioItem key={`topnav-theme-${theme.value}`} value={theme.value}>
|
||||
{theme.name}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
@@ -6,16 +6,16 @@ import {
|
||||
Button_Shadcn_ as Button,
|
||||
cn,
|
||||
Command,
|
||||
CommandGroup as CommandGroup,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem as CommandItem,
|
||||
CommandList as CommandList,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollArea,
|
||||
} from 'ui'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
export interface ComboBoxOption {
|
||||
id: string
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
useProjectsInfiniteQuery,
|
||||
} from '~/lib/fetch/projects-infinite'
|
||||
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import { useOnLogout } from '~/lib/userAuth'
|
||||
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
@@ -389,12 +390,13 @@ function VariableView({ variable, className }: { variable: Variable; className?:
|
||||
}
|
||||
|
||||
const { copied, handleCopy } = useCopy()
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
<Input
|
||||
disabled
|
||||
readOnly
|
||||
type="text"
|
||||
className="font-mono"
|
||||
value={
|
||||
@@ -413,10 +415,16 @@ function VariableView({ variable, className }: { variable: Variable; className?:
|
||||
disabled={!variableValue}
|
||||
variant="ghost"
|
||||
className="px-0"
|
||||
onClick={handleCopy}
|
||||
onClick={() => {
|
||||
handleCopy()
|
||||
sendTelemetryEvent({
|
||||
action: 'docs_project_config_variables_copy_button_clicked',
|
||||
properties: { variable },
|
||||
})
|
||||
}}
|
||||
aria-label="Copy"
|
||||
>
|
||||
{copied ? <Check /> : <Copy />}
|
||||
{copied ? <Check size="18" /> : <Copy size="18" />}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
@@ -445,7 +453,7 @@ function LoginHint({ variable }: { variable: Variable }) {
|
||||
if (isUserLoading || isLoggedIn) return null
|
||||
|
||||
return (
|
||||
<p className="text-foreground-muted text-sm mt-2 mb-0 ml-1">
|
||||
<p className="not-prose text-foreground-muted text-sm mt-2 mb-0 ml-1">
|
||||
To get your {prettyFormatVariable[variable]},{' '}
|
||||
<Link
|
||||
className="text-foreground-muted"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants'
|
||||
import Link from 'next/link'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
const SkipToContent = () => {
|
||||
return (
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="default"
|
||||
asChild
|
||||
className={cn(
|
||||
'fixed top-0 left-4 z-[100] w-auto',
|
||||
'-translate-y-full focus-visible:translate-y-4',
|
||||
'transition-transform duration-200 ease-out'
|
||||
)}
|
||||
>
|
||||
<Link href={`#${DOCS_CONTENT_CONTAINER_ID}`}>Skip to content</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export { SkipToContent }
|
||||
@@ -80,7 +80,7 @@ const Step: FC<PropsWithChildren<IStep>> = ({ children, title, step }) => {
|
||||
const Details: FC<PropsWithChildren<IDetails>> = ({ children, title, fullWidth = false }) => {
|
||||
return (
|
||||
<div className={cn(fullWidth ? 'col-span-12' : 'col-span-5', 'ml-12', 'lg:ml-0')}>
|
||||
<h3 className="mt-0 text-foreground text-base">{title}</h3>
|
||||
<h3 className="not-prose mb-4 text-foreground text-base">{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide.
|
||||
|
||||
Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.
|
||||
Alternatively if you would like to deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.
|
||||
@@ -57,6 +57,12 @@ Some endpoints have stricter rate limits than the standard 120 requests per minu
|
||||
|
||||
**Note:** The `GET /v1/projects/:ref/database/context` endpoint has dual rate limiting. You can make up to 10 requests per minute, but also no more than 1 request per second to prevent burst traffic.
|
||||
|
||||
Some endpoints have the standard 120 requests per minute but with different timeout durations:
|
||||
|
||||
| Endpoint | Limit | Duration | Reason |
|
||||
| -------------------------------------------- | ------------ | --------- | ---------------------------------------------------- |
|
||||
| `POST /v1/projects/:ref/database/migrations` | 120 requests | 3 minutes | Database migrations may require more processing time |
|
||||
|
||||
### Best practices
|
||||
|
||||
- **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
### Get API details
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
To interact with data in database tables, you use the client libraries that wrap [the auto-generated Data API endpoints](/docs/guides/api), authenticating using the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).
|
||||
|
||||
To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).
|
||||
<ProjectConfigVariables variable="url" />
|
||||
<ProjectConfigVariables variable="publishable" />
|
||||
|
||||
[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.
|
||||
<Admonition type="tip">
|
||||
|
||||
<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }}
|
||||
/>
|
||||
[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types, their uses, and where to find them.
|
||||
|
||||
</Admonition>
|
||||
@@ -1,12 +0,0 @@
|
||||
{/* TODO: How to completely consolidate partials? */}
|
||||
|
||||
### Get API details
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
|
||||
To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).
|
||||
|
||||
[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.
|
||||
|
||||
<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }}
|
||||
/>
|
||||
@@ -22,7 +22,7 @@ on todos for select
|
||||
using ( set_information() AND (select auth.uid()) = user_id );
|
||||
```
|
||||
|
||||
This ensures the function is called when evaluating RLS policies for all products, not just Data API requests.
|
||||
This ensures the function is called when evaluating RLS policies for all products, not only Data API requests.
|
||||
|
||||
**Performance consideration:**
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Before building, you must set up your Database and API with a new Project in Sup
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. You can just copy/paste the SQL from below and run it yourself.
|
||||
Now we are going to set up the database schema. You can copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<Admonition type="note">
|
||||
|
||||
This default takes effect for new projects from July 9, 2026.
|
||||
|
||||
</Admonition>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
Add the Postgres binary to your system PATH.
|
||||
|
||||
In Control Panel, under the Advanced tab of System Properties, click Environment Variables. Edit the Path variable by adding the path to the SQL binary you just installed.
|
||||
In Control Panel, under the Advanced tab of System Properties, click Environment Variables. Edit the Path variable by adding the path to the SQL binary you installed.
|
||||
|
||||
The path will look something like this, though it may differ slightly depending on your installed version:
|
||||
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
<StepHikeCompact.Details title="Create a Supabase project">
|
||||
export const sqlSetup = ` -- Create the table
|
||||
create table instruments (
|
||||
id bigint primary key generated always as identity,
|
||||
name text not null
|
||||
);
|
||||
|
||||
Go to [database.new](https://database.new) and create a new Supabase project.
|
||||
-- Insert sample data into the table
|
||||
insert into instruments (name)
|
||||
values
|
||||
('violin'),
|
||||
('viola'),
|
||||
('cello');
|
||||
|
||||
Alternatively, you can create a project using the Management API:
|
||||
-- Grant the privileges the role needs, which is read access
|
||||
grant select on public.instruments to anon;
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
-- Enable row level security for the table
|
||||
alter table instruments enable row level security;
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
-- Create a policy to allow the anon role to read from the instruments table
|
||||
create policy "public can read instruments"
|
||||
on public.instruments
|
||||
for select to anon
|
||||
using (true);`
|
||||
|
||||
## 1. Create a Supabase project
|
||||
|
||||
Before you can use Supabase, you need a Supabase project. You can create a project visually in the Dashboard or programmatically using [the Management API](/docs/reference/api/introduction).
|
||||
|
||||
<Tabs scrollable size="small" type="underlined" defaultActiveId="dashboard" queryGroup="create-project">
|
||||
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
Create a new Supabase project from [the Dashboard of any organization](/dashboard/new/_) you belong to.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
You can also use [database.new](https://database.new) to create a new Supabase project.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="management-api" label="Management API">
|
||||
|
||||
First, get your access token from the [**Account Settings > Access Tokens**](/dashboard/account/tokens) section of the Dashboard.
|
||||
|
||||
Then create a project using [the Management API](/docs/reference/api/introduction):
|
||||
|
||||
```bash
|
||||
# First, get your access token from https://supabase.com/dashboard/account/tokens
|
||||
export SUPABASE_ACCESS_TOKEN="your-access-token"
|
||||
|
||||
# List your organizations to get the organization ID
|
||||
@@ -28,19 +66,36 @@ curl -X POST https://api.supabase.com/v1/projects \
|
||||
}'
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
</TabPanel>
|
||||
|
||||
<StepHikeCompact.Details>
|
||||
</Tabs>
|
||||
|
||||
When your project is up and running, go to the [**Table Editor**](/dashboard/project/_/editor) section of the Dashboard, create a new table and insert some data. Then in the [**Integrations > Data API**](/dashboard/project/_/integrations/data_api/settings) section of the Dashboard, expose the specific tables or functions you want to access. To automatically grant access for new tables and functions in `public`, enable **Default privileges for new entities**.
|
||||
## 2. Set up your database
|
||||
|
||||
Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new).
|
||||
When your Supabase project is up and running, create an `instruments` table with some sample data.
|
||||
|
||||
This creates an `instruments` table with some sample data, sets a secure baseline by setting only the privileges each Postgres role needs, and adds [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default.
|
||||
Then set a secure baseline by setting only the privileges each Postgres role needs, add [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default, and create an RLS policy to make the data in your table publicly readable.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
You can click this button to prefill all the SQL needed in [the SQL editor of your project](/dashboard/project/_/sql) in the Dashboard.
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
<Button variant="primary" asChild>
|
||||
<a href={`/dashboard/project/_/sql/new?content=${encodeURIComponent(sqlSetup)}`}>Prefill SQL</a>
|
||||
</Button>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="single"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light mt-8 mb-6"
|
||||
>
|
||||
<AccordionItem
|
||||
header="Want to learn more about the SQL and add it manually?"
|
||||
id="manual-sql"
|
||||
>
|
||||
|
||||
Run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). You can also choose the SQL and prefill it from the **Reference > Samples** menu at the side of the SQL Editor.
|
||||
|
||||
```sql SQL_EDITOR
|
||||
-- Create the table
|
||||
@@ -61,19 +116,7 @@ This creates an `instruments` table with some sample data, sets a secure baselin
|
||||
|
||||
-- Enable row level security for the table
|
||||
alter table instruments enable row level security;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details>
|
||||
|
||||
Create an RLS policy to make the data in your table publicly readable:
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql SQL_EDITOR
|
||||
-- Create a policy to allow the anon role to read from the instruments table
|
||||
create policy "public can read instruments"
|
||||
on public.instruments
|
||||
@@ -81,4 +124,12 @@ Create an RLS policy to make the data in your table publicly readable:
|
||||
using (true);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Admonition type="note" label="Disabled the Data API during project setup?">
|
||||
|
||||
If you disabled the Data API during project setup, enable it in the [**Integrations > Data API**](/dashboard/project/_/integrations/data_api/settings) section of the Dashboard and expose the specific tables or functions you want to access. To automatically grant access for new tables and functions in `public`, enable **Automatically expose new tables**.
|
||||
|
||||
</Admonition>
|
||||
@@ -120,8 +120,10 @@ It is possible to upload more vectors to a single table if Memory allows it (for
|
||||
|
||||
</Admonition>
|
||||
|
||||
The chart below compares HNSW queries-per-second across compute sizes for different embedding dimensions.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart comparing HNSW queries-per-second across Supabase compute sizes for different embedding dimensions."
|
||||
src={{
|
||||
light: '/docs/img/ai/instance-type/hnsw-dims--light.png',
|
||||
dark: '/docs/img/ai/instance-type/hnsw-dims--dark.png',
|
||||
@@ -256,8 +258,10 @@ For 1,000,000 vectors 40 probes results to accuracy of 0.98. Note that exact val
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
The chart below plots requests-per-second against compute size.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart plotting requests-per-second against Supabase compute size."
|
||||
src={{
|
||||
light: '/docs/img/ai/going-prod/size-to-rps--light.png',
|
||||
dark: '/docs/img/ai/going-prod/size-to-rps--dark.png',
|
||||
@@ -294,8 +298,10 @@ You can increase the Requests per Second by increasing `m` and `ef_construction`
|
||||
>
|
||||
<TabPanel id="hnsw" label="HNSW">
|
||||
|
||||
The chart below shows how the HNSW build parameters `m` and `ef_construction` affect requests-per-second on the dbpedia dataset.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart showing how HNSW build parameters (m and ef_construction) affect requests-per-second on the dbpedia dataset."
|
||||
src={{
|
||||
light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png',
|
||||
dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png',
|
||||
@@ -308,8 +314,10 @@ You can increase the Requests per Second by increasing `m` and `ef_construction`
|
||||
</TabPanel>
|
||||
<TabPanel id="ivfflat" label="IVFFlat">
|
||||
|
||||
The chart below shows how the number of IVFFlat lists affects performance for one million vectors.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart showing how the number of IVFFlat lists affects performance for 1 million vectors."
|
||||
src={{
|
||||
light: '/docs/img/ai/instance-type/lists-for-1m--light.png',
|
||||
dark: '/docs/img/ai/instance-type/lists-for-1m--dark.png',
|
||||
@@ -329,7 +337,7 @@ Check out more tips and the complete step-by-step guide in [Going to Production
|
||||
We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology. A Python test runner is responsible for uploading the data, creating the index, and running the queries. The pgvector engine is implemented using [vecs](https://github.com/supabase/vecs), a Python client for pgvector.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Diagram of the vecs benchmark setup: a Python test runner uploads data, builds the index, and runs queries against pgvector."
|
||||
src={{
|
||||
light: '/docs/img/ai/instance-type/vecs-benchmark--light.png',
|
||||
dark: '/docs/img/ai/instance-type/vecs-benchmark--dark.png',
|
||||
@@ -340,6 +348,8 @@ We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikber
|
||||
|
||||
/>
|
||||
|
||||
_The diagram above shows the vecs benchmark setup: a Python test runner uploads data, builds the index, and runs queries against pgvector._
|
||||
|
||||
Each test is run for a minimum of 30-40 minutes. They include a series of experiments executed at different concurrency levels to measure the engine's performance under different load types. The results are then averaged.
|
||||
|
||||
As a general recommendation, we suggest using a concurrency level of 5 or more for most workloads and 30 or more for high-load workloads.
|
||||
@@ -39,7 +39,14 @@ So how does this relate to embeddings?
|
||||
|
||||
Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this:
|
||||
|
||||
<img src="/docs/img/ai/vector-similarity.png" alt="Vector similarity" width="640" height="640" />
|
||||
The chart below plots example phrases as points. Phrases with similar meanings sit close together, and unrelated phrases sit far apart.
|
||||
|
||||
<img
|
||||
src="/docs/img/ai/vector-similarity.png"
|
||||
alt="A two-dimensional chart plotting example phrases as points, where phrases with similar meanings sit close together and unrelated phrases sit far apart."
|
||||
width="640"
|
||||
height="640"
|
||||
/>
|
||||
|
||||
Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese).
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ For small workloads, you can typically store your data in a single database.
|
||||
|
||||
If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views):
|
||||
|
||||
The diagram below shows a single database holding the three vector collections of `docs`, `posts`, and `images`. Each are exposed to your application through a view.
|
||||
|
||||
<Image
|
||||
alt="single database"
|
||||
alt="Architecture diagram: a single Supabase database holding three vector collections (docs, posts, and images), each exposed to the application through a view."
|
||||
src={{
|
||||
light: '/docs/img/ai/scaling/engineering-for-scale--single-database--light.png',
|
||||
dark: '/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png',
|
||||
@@ -51,8 +53,10 @@ const { data, error } = await supabase
|
||||
|
||||
As you move into production, we recommend splitting your collections into separate projects. This is because it allows your vector stores to scale independently of your production data. Vectors typically grow faster than operational data, and they have different resource requirements. Running them on separate databases removes the single-point-of-failure.
|
||||
|
||||
The diagram below shows a primary database alongside separate secondary "pod" databases, each holding its own vector collection so they can scale independently.
|
||||
|
||||
<Image
|
||||
alt="With secondaries"
|
||||
alt="Architecture diagram: a primary database alongside separate secondary 'pod' databases, each holding its own vector collection so collections can scale independently."
|
||||
src={{
|
||||
light: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png',
|
||||
dark: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png',
|
||||
@@ -134,10 +138,10 @@ const { data, error } = await supabase
|
||||
|
||||
### Enterprise architecture
|
||||
|
||||
This diagram provides an example architecture that allows you to access the collections either with our client libraries or using Vecs. You can add as many secondary databases as you need (in this example we only show one):
|
||||
This diagram below provides an example architecture that allows you to access the collections either with our client libraries or using Vecs. You can add as many secondary databases as you need (in this example we only show one):
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Enterprise architecture diagram: an application accessing vector collections in multiple secondary databases, either directly via Vecs or through the primary database using Foreign Data Wrappers."
|
||||
src={{
|
||||
light: '/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png',
|
||||
dark: '/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png',
|
||||
|
||||
@@ -82,13 +82,13 @@ To start quicker you may use Supabase CLI to spin everything up locally as it al
|
||||
supabase start
|
||||
```
|
||||
|
||||
This will pull all docker images and run Supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
|
||||
This will pull all docker images and run Supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way: export the environment variables and follow to the next steps.
|
||||
|
||||
Using `supabase-cli` is not required and you can use any other docker image or hosted version of Postgres that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
|
||||
Using `supabase-cli` is not required and you can use any other docker image or hosted version of Postgres that includes `pgvector`. Make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
|
||||
|
||||
### Step 5: Obtain OpenAI API key
|
||||
|
||||
To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, you just need to go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.
|
||||
To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ breadcrumb: 'AI Examples'
|
||||
|
||||
Supabase provides a [Headless Search Toolkit](https://github.com/supabase/headless-vector-search) for adding "Generative Q&A" to your documentation. The toolkit is "headless", so that you can integrate it into your existing website and style it to match your website theme.
|
||||
|
||||
You can see how this works with the Supabase docs. Just hit `cmd+k` and "ask" for something like "what are the features of Supabase?". You will see that the response is streamed back, using the information provided in the docs:
|
||||
You can see how this works with the Supabase docs. Enter `cmd+k` and ask, for example, "what are the features of Supabase?". You will see that the response is streamed back using the information provided in the docs:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -168,4 +168,4 @@ Go ahead and test it out by running `poetry run search` and you will be presente
|
||||
|
||||
## Conclusion
|
||||
|
||||
With just a couple of lines of Python you are able to implement image search as well as reverse image search using OpenAI's CLIP model and Supabase Vector.
|
||||
With a couple of lines of Python you are able to implement image search as well as reverse image search using OpenAI's CLIP model and Supabase Vector.
|
||||
@@ -175,4 +175,4 @@ You can now test it out by running `poetry run search`, and you will be presente
|
||||
|
||||
## Conclusion
|
||||
|
||||
With just a couple of Python scripts, you are able to implement video search as well as reverse video search using Mixpeek Embed and Supabase Vector. This approach allows for powerful semantic search capabilities that can be integrated into various applications, enabling you to search through video content using both text and video queries.
|
||||
With a couple of Python scripts, you are able to implement video search as well as reverse video search using Mixpeek Embed and Supabase Vector. This approach allows for semantic search capabilities that can be integrated into various applications, enabling you to search through video content using both text and video queries.
|
||||
@@ -231,4 +231,4 @@ Go ahead and test it out by running `poetry run search` and you will be presente
|
||||
|
||||
## Conclusion
|
||||
|
||||
With just a couple of lines of Python you are able to implement image search as well as reverse image search using the Amazon Titan multimodal model and Supabase Vector.
|
||||
With a couple of lines of Python you are able to implement image search as well as reverse image search using the Amazon Titan multimodal model and Supabase Vector.
|
||||
@@ -74,8 +74,10 @@ The values of lists and probes directly affect accuracy and queries per second (
|
||||
|
||||
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blogpost.
|
||||
|
||||
The chart below shows how the IVFFlat lists count affects accuracy and queries-per-second.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart showing how the IVFFlat lists count affects query accuracy and queries-per-second."
|
||||
src={{
|
||||
light: '/docs/img/ai/going-prod/lists-count--light.png',
|
||||
dark: '/docs/img/ai/going-prod/lists-count--dark.png',
|
||||
@@ -117,8 +119,10 @@ You can look at our [Choosing Compute Add-on](/docs/guides/ai/choosing-compute-a
|
||||
|
||||
Or take a look at our [pgvector 0.5.0 performance](/blog/increase-performance-pgvector-hnsw) and [pgvector 0.4.0 performance](/blog/pgvector-performance) blog posts to see what pgvector is capable of and how the above technique can be used to achieve the best results.
|
||||
|
||||
The chart below plots requests-per-second against compute size.
|
||||
|
||||
<Image
|
||||
alt="multi database"
|
||||
alt="Chart plotting requests-per-second against Supabase compute size."
|
||||
src={{
|
||||
light: '/docs/img/ai/going-prod/size-to-rps--light.png',
|
||||
dark: '/docs/img/ai/going-prod/size-to-rps--dark.png',
|
||||
|
||||
@@ -16,7 +16,7 @@ Hybrid search combines the strengths of both these methods. It would ensure that
|
||||
|
||||
## When to consider hybrid search
|
||||
|
||||
The decision to use hybrid search depends on what your users are looking for in your app. For a code repository where developers need to find exact lines of code or error messages, keyword search is likely ideal because it matches specific terms. In a mental health forum where users search for advice or experiences related to their feelings, semantic search may be better because it finds results based on the meaning of a query, not just specific words. For a shopping app where customers might search for specific product names yet also be open to related suggestions, hybrid search combines the best of both worlds - finding exact matches while also uncovering similar products based on the shopping context.
|
||||
The decision to use hybrid search depends on what your users are looking for in your app. For a code repository where developers need to find exact lines of code or error messages, keyword search is likely ideal because it matches specific terms. In a mental health forum where users search for advice or experiences related to their feelings, semantic search may be better because it finds results based on the meaning of a query, not only specific words. For a shopping app where customers might search for specific product names yet also be open to related suggestions, hybrid search combines the best of both worlds - finding exact matches while also uncovering similar products based on the shopping context.
|
||||
|
||||
## How to combine search methods
|
||||
|
||||
@@ -46,7 +46,7 @@ This constant can be any positive number, but is typically small. A constant of
|
||||
|
||||
Implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search).
|
||||
|
||||
First we'll create a `documents` table to store the documents that we will search over. This is just an example - adjust this to match the structure of your application.
|
||||
First, you can create a `documents` table to store the documents that you can search over. This is an example. Adjust this to match the structure of your application.
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
|
||||
@@ -7,7 +7,7 @@ sidebar_label: 'Choosing a Client'
|
||||
|
||||
As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured), AI workloads come in many forms.
|
||||
|
||||
For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata.
|
||||
For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started. You need a connection string and vecs handles setting up your database to store and query vectors with associated metadata.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ create table documents (
|
||||
);
|
||||
```
|
||||
|
||||
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 384 dimensions. Change this to the number of dimensions produced by your embedding model. For example, if you are [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, you would set this number to 384 since that model produces 384 dimensions.
|
||||
In the SQL snippet above, we create a `documents` table with an `embedding` column. This is a standard Postgres column, so you can name it anything you like. The `embedding` column uses the `vector` data type with 384 dimensions. Change this number to match the dimensions your embedding model produces. For example, if you're [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, set this to 384.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
@@ -73,6 +73,7 @@ In this example we'll generate a vector using Transformers.js, then store it in
|
||||
|
||||
```js
|
||||
import { pipeline } from '@huggingface/transformers'
|
||||
|
||||
const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small')
|
||||
|
||||
const title = 'First post!'
|
||||
|
||||
@@ -75,8 +75,10 @@ The hierarchical aspect of HNSW builds off of the idea of skip lists.
|
||||
|
||||
Skip lists are multi-layer linked lists. The bottom layer is a regular linked list connecting an ordered sequence of elements. Each new layer above removes some elements from the underlying layer (based on a fixed probability), producing a sparser subsequence that “skips” over elements.
|
||||
|
||||
The diagram below shows a multi-layer skip list. The bottom layer links every ordered element, and each layer above keeps a sparser subset that skips over elements.
|
||||
|
||||
<Image
|
||||
alt="visual of an example skip list"
|
||||
alt="Diagram of a multi-layer skip list: the bottom layer is a linked list of all ordered elements, and each higher layer keeps a sparser subset that skips over elements."
|
||||
src={{
|
||||
light: '/docs/img/ai/vector-indexes/hnsw-indexes/skip-list--light.png',
|
||||
dark: '/docs/img/ai/vector-indexes/hnsw-indexes/skip-list--dark.png',
|
||||
@@ -91,8 +93,10 @@ When searching for an element, the algorithm begins at the top layer and travers
|
||||
|
||||
A navigable small world (NSW) is a special type of proximity graph that also includes long-range connections between nodes. These long-range connections support the “small world” property of the graph, meaning almost every node can be reached from any other node within a few hops. Without these additional long-range connections, many hops would be required to reach a far-away node.
|
||||
|
||||
The diagram below shows a navigable small world graph. Each node connects to nearby neighbors plus a few long-range links, so almost any node can reach any other in a few hops.
|
||||
|
||||
<Image
|
||||
alt="visual of an example navigable small world graph"
|
||||
alt="Diagram of a navigable small world graph, where each node connects to nearby neighbors plus a few long-range links that let almost any node reach any other within a few hops."
|
||||
src="/docs/img/ai/vector-indexes/hnsw-indexes/nsw.png"
|
||||
className="max-h-[600px] mx-auto"
|
||||
width={1016}
|
||||
@@ -106,7 +110,7 @@ The “navigable” part of NSW specifically refers to the ability to logarithmi
|
||||
|
||||
HNSW combines these two concepts. From the hierarchical perspective, the bottom layer consists of a NSW made up of short links between nodes. Each layer above “skips” elements and creates longer links between nodes further away from each other.
|
||||
|
||||
Just like skip lists, search starts at the top layer and works its way down until it finds the target element. However, instead of comparing a scalar value at each layer to determine whether or not to descend to the layer below, a multi-dimensional distance measure (such as Euclidean distance) is used.
|
||||
Like skip lists, search starts at the top layer and works its way down until it finds the target element. However, instead of comparing a scalar value at each layer to determine whether or not to descend to the layer below, a multi-dimensional distance measure (such as Euclidean distance) is used.
|
||||
|
||||
## When should you create HNSW indexes?
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 'Handling errors in `supabase-js`'
|
||||
subtitle: 'Read `error.hint` first — Postgres often tells you the exact fix. Log the full error so you actually see it.'
|
||||
---
|
||||
|
||||
Every `supabase-js` call returns a `{ data, error }` pair instead of throwing. When something fails, the single most useful field on `error` is usually `hint` — Postgres returns the _fix_, not just a description of the problem. Logging only `error.message` hides it.
|
||||
Every `supabase-js` call returns a `{ data, error }` pair instead of throwing. When something fails, the single most useful field on `error` is usually `hint` — Postgres returns the _fix_, not only a description of the problem. Logging only `error.message` hides it.
|
||||
|
||||
## Usage of `message` and `hint` properties
|
||||
|
||||
@@ -19,7 +19,7 @@ The `message` exposes the error reason, and `hint` gives you the literal SQL sta
|
||||
|
||||
The same pattern shows up across many Postgres errors — missing column? `hint` suggests the column name you probably meant. Type mismatch? `hint` shows the expected type. Whenever Postgres knows the fix, it puts it in `hint`.
|
||||
|
||||
<Admonition type="tip">Log the full `error` object, not just `error.message`.</Admonition>
|
||||
<Admonition type="tip">Log the full `error` object, not only `error.message`.</Admonition>
|
||||
|
||||
## The recommended pattern
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Any table created through the Supabase Dashboard will have RLS enabled by defaul
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Authentication > Policies](/dashboard/project/_/auth/policies) page in the Dashboard.
|
||||
1. Go to the [Database > Policies](/dashboard/project/_/database/policies) page in the Dashboard.
|
||||
2. Select **Enable RLS** to enable Row Level Security.
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: 'auth'
|
||||
title: 'Auth'
|
||||
description: 'Use Supabase to Authenticate and Authorize your users.'
|
||||
description: 'Use Supabase to authenticate and authorize your users.'
|
||||
subtitle: 'Use Supabase to authenticate and authorize your users.'
|
||||
tocVideo: '6ow_jW4epf8'
|
||||
---
|
||||
@@ -27,19 +27,15 @@ Auth uses your project's Postgres database under the hood, storing user data and
|
||||
|
||||
Auth also enables access control to your database's automatically generated [REST API](/docs/guides/api). When using Supabase SDKs, your data requests are automatically sent with the user's Auth Token. The Auth Token scopes database access on a row-by-row level when used along with [RLS policies](/docs/guides/database/postgres/row-level-security).
|
||||
|
||||
<ContentListings id="auth-get-started" />
|
||||
|
||||
<$Show if="authentication:show_providers">
|
||||
<$Partial path="providers.mdx" />
|
||||
</$Show>
|
||||
|
||||
<$Show if="billing:all">
|
||||
|
||||
## Pricing
|
||||
|
||||
Charges apply to Monthly Active Users (MAU), Monthly Active Third-Party Users (Third-Party MAU), and Monthly Active SSO Users (SSO MAU) and Advanced MFA Add-ons. For a detailed breakdown of how these charges are calculated, refer to the following pages:
|
||||
|
||||
- [Pricing MAU](/docs/guides/platform/manage-your-usage/monthly-active-users)
|
||||
- [Pricing Third-Party MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party)
|
||||
- [Pricing SSO MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-sso)
|
||||
- [Advanced MFA - Phone](/docs/guides/platform/manage-your-usage/advanced-mfa-phone)
|
||||
|
||||
<ContentListings id="auth-pricing" />
|
||||
</$Show>
|
||||
|
||||
<ContentListings id="auth-next-steps" />
|
||||
@@ -8,7 +8,7 @@ subtitle: 'Create and use anonymous users to authenticate with Supabase'
|
||||
|
||||
<Admonition type="note" title="Anonymous user vs the anon key">
|
||||
|
||||
Calling `signInAnonymously()` creates an anonymous user. It's just like a permanent user, except the user can't access their account if they sign out, clear browsing data, or use another device.
|
||||
Calling `signInAnonymously()` creates an anonymous user. It behaves like a permanent user, except the user can't access their account if they sign out, clear browsing data, or use another device.
|
||||
|
||||
Like permanent users, the `authenticated` Postgres role will be used when using the Data APIs to access your project. JWTs for these users will have an `is_anonymous` claim which you can use to distinguish in RLS policies.
|
||||
|
||||
@@ -279,7 +279,7 @@ response = supabase.auth.link_identity({'provider': 'google'})
|
||||
|
||||
## Access control
|
||||
|
||||
An anonymous user assumes the `authenticated` role just like a permanent user. You can use row-level security (RLS) policies to differentiate between an anonymous user and a permanent user by checking for the `is_anonymous` claim in the JWT returned by `auth.jwt()`:
|
||||
An anonymous user assumes the `authenticated` role like a permanent user. You can use row-level security (RLS) policies to differentiate between an anonymous user and a permanent user by checking for the `is_anonymous` claim in the JWT returned by `auth.jwt()`:
|
||||
|
||||
```sql
|
||||
create policy "Only permanent users can post to the news feed"
|
||||
|
||||
@@ -87,7 +87,7 @@ Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linki
|
||||
|
||||
```dart
|
||||
Future<void> signInWithEmail() async {
|
||||
final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
|
||||
await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
|
||||
}
|
||||
```
|
||||
|
||||
@@ -223,7 +223,7 @@ const { data, error } = await supabase.auth.signInWithOtp({
|
||||
|
||||
```dart
|
||||
Future<void> signInWithEmailOtp() async {
|
||||
final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
|
||||
await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -49,7 +49,27 @@ The templating system provides the following variables for use:
|
||||
|
||||
## Editing email templates
|
||||
|
||||
On hosted Supabase projects, edit your email templates on the [Email Templates](/dashboard/project/_/auth/templates) page. On self-hosted projects or in local development, edit your [configuration files](/docs/guides/local-development/customizing-email-templates).
|
||||
Where you edit templates depends on how you run Supabase.
|
||||
|
||||
### Hosted projects
|
||||
|
||||
Edit templates on the [Email Templates](/dashboard/project/_/auth/templates) page in the dashboard. The template builder uses the same [terminology](#terminology) and variables documented on this page.
|
||||
|
||||
### Local development and self-hosted
|
||||
|
||||
The dashboard template builder does not apply when running Supabase locally or self-hosted. Customize templates in `supabase/config.toml` and HTML files instead.
|
||||
|
||||
<Admonition type="note" title="Customizing templates locally">
|
||||
|
||||
See [Customizing email templates](/docs/guides/local-development/customizing-email-templates) for:
|
||||
|
||||
- [`config.toml` keys and HTML file paths](/docs/guides/local-development/customizing-email-templates#configuring-templates)
|
||||
- [Template variables](/docs/guides/local-development/customizing-email-templates#template-variables) — the same placeholders as the [terminology](#terminology) table above
|
||||
- Default subjects and behavior for each [authentication](/docs/guides/local-development/customizing-email-templates#available-authentication-email-templates) and [security notification](/docs/guides/local-development/customizing-email-templates#available-security-notification-email-templates) template
|
||||
|
||||
For production self-hosted deployments, see [Custom email templates](/docs/guides/self-hosting/custom-email-templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can also manage email templates using the Management API:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Supabase Auth will send a payload containing these fields to your hook:
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Because the hook is ran just before the insertion into the database, this user will not be found in Postgres at the time the hook is called.
|
||||
Because the hook runs immediately before insertion into the database, this user will not be found in Postgres at the time the hook is called.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -722,8 +722,8 @@ supabase functions new before-user-created-hook
|
||||
Add the following code to your edge function:
|
||||
|
||||
```ts
|
||||
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js'
|
||||
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
||||
|
||||
const whSecret = Deno.env.get('BEFORE_USER_CREATED_HOOK_SECRET')?.replace('v1,whsec_', '')
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
|
||||
@@ -271,7 +271,7 @@ It is possible to enforce MFA on the Server-Side Rendering level. However, this
|
||||
|
||||
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and `supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session and any factors that are enabled for a user, similar to how you would use these on the browser.
|
||||
|
||||
However, encountering a different AAL level on the server may not actually be a security problem. Consider these likely scenarios:
|
||||
However, encountering a different AAL level on the server may not be a security problem. Consider these likely scenarios:
|
||||
|
||||
1. User signed-in with a conventional method but closed their tab on the MFA
|
||||
flow.
|
||||
@@ -284,7 +284,7 @@ We thus recommend you redirect users to a page where they can authenticate using
|
||||
|
||||
### APIs
|
||||
|
||||
If your application uses the Supabase Database, Storage or Edge Functions, just using Row Level Security policies will give you sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines:
|
||||
If your application uses the Supabase Database, Storage or Edge Functions, Row Level Security policies provide sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines:
|
||||
|
||||
1. **Use a good JWT verification and parsing library for your language.**
|
||||
This will let you securely parse JWTs and extract their claims.
|
||||
|
||||
@@ -12,23 +12,36 @@ The phone messaging configuration for MFA is shared with [phone auth login](/doc
|
||||
|
||||
Below is a flow chart illustrating how the Enrollment and Verify APIs work in the context of MFA (Phone).
|
||||
|
||||
<Image
|
||||
alt="Diagram showing the flow of Multi-Factor authentication"
|
||||
src={{
|
||||
light: '/docs/img/guides/auth-mfa/auth-mfa-phone-flow.svg',
|
||||
dark: '/docs/img/guides/auth-mfa/auth-mfa-phone-flow.svg',
|
||||
}}
|
||||
containerClassName="max-w-[700px]"
|
||||
width={93}
|
||||
height={150}
|
||||
/>
|
||||
```mermaid
|
||||
flowchart TD
|
||||
InitS((Setup flow)) --> SAAL1[/Session is AAL1/]
|
||||
SAAL1 --> Enroll[Enroll API]
|
||||
Enroll --> ChallengeAPI[Challenge API]
|
||||
ChallengeAPI --> Scan[/Code sent to User/]
|
||||
Scan --> Enter[User: Enter code]
|
||||
Enter --> Verify[Verify API]
|
||||
Verify --> Check{{Is code correct?}}
|
||||
Check -->|Yes| AAL2[/Upgrade to AAL2/]
|
||||
AAL2 --> Done((Done))
|
||||
Check -->|No| Enter
|
||||
InitA((Login flow)) --> SignIn([User: Sign-in])
|
||||
SignIn --> AAL1[/Upgrade to AAL1/]
|
||||
AAL1 --> ListFactors[List Factors API]
|
||||
ListFactors -->|1 or more factors| OpenAuth([User: Select phone factor])
|
||||
OpenAuth --> Enter
|
||||
ListFactors -->|0 factors| Setup[[Setup flow]]
|
||||
```
|
||||
|
||||
In the **setup flow**, a session already at AAL1 calls the Enroll API followed by the Challenge API, which sends a code to the user over SMS or WhatsApp. The user enters the code, the Verify API checks it, and on success the session is upgraded to AAL2. An incorrect code returns the user to the code-entry step.
|
||||
|
||||
In the **login flow**, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they select their phone factor and enter the code that was sent, following the same Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first.
|
||||
|
||||
### Add enrollment flow
|
||||
|
||||
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
|
||||
|
||||
1. Right after login or sign up.
|
||||
This allows users quickly set up Multi Factor Authentication (MFA) post login or account creation. Where possible, encourage all users to set up MFA. Many applications offer this as an opt-in step in an
|
||||
This allows users to set up Multi Factor Authentication (MFA) post login or account creation. Where possible, encourage all users to set up MFA. Many applications offer this as an opt-in step in an
|
||||
effort to reduce onboarding friction.
|
||||
2. From within a settings page.
|
||||
Allows users to set up, disable or modify their MFA settings.
|
||||
|
||||
@@ -12,25 +12,42 @@ The use of a QR code was [initially introduced by Google Authenticator](https://
|
||||
|
||||
Below is a flow chart illustrating how the Enrollment, Challenge, and Verify APIs work in the context of MFA (TOTP).
|
||||
|
||||
<Image
|
||||
alt="Diagram showing the flow of Multi-Factor authentication"
|
||||
src={{
|
||||
light: '/docs/img/guides/auth-mfa/auth-mfa-flow.svg',
|
||||
dark: '/docs/img/guides/auth-mfa/auth-mfa-flow.svg',
|
||||
}}
|
||||
containerClassName="max-w-[700px]"
|
||||
width={111}
|
||||
height={150}
|
||||
/>
|
||||
```mermaid
|
||||
flowchart TD
|
||||
InitS((Setup flow)) --> SAAL1[/Session is AAL1/]
|
||||
SAAL1 --> Enroll[Enroll API]
|
||||
Enroll --> ShowQR[Show QR code]
|
||||
ShowQR --> Scan([User: Scan QR code in authenticator])
|
||||
Scan --> Enter([User: Enter code])
|
||||
Enter --> Verify[Challenge + Verify API]
|
||||
Verify --> Check{{Is code correct?}}
|
||||
Check -->|Yes| AAL2[/Upgrade to AAL2/]
|
||||
AAL2 --> Done((Done))
|
||||
Check -->|No| Enter
|
||||
InitA((Login flow)) --> SignIn([User: Sign-in])
|
||||
SignIn --> AAL1[/Upgrade to AAL1/]
|
||||
AAL1 --> ListFactors[List Factors API]
|
||||
ListFactors -->|1 or more factors| OpenAuth([User: Open authenticator])
|
||||
OpenAuth --> Enter
|
||||
ListFactors -->|0 factors| Setup[[Setup flow]]
|
||||
```
|
||||
|
||||
In the **setup flow**, a session already at AAL1 calls the Enroll API, which returns a QR code for the user to scan with their authenticator app. The user enters the generated code, the Challenge and Verify APIs check it, and on success the session is upgraded to AAL2. If the code is incorrect, the user is prompted to enter it again.
|
||||
|
||||
In the **login flow**, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they open their authenticator and enter a code, which follows the same Challenge and Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
[TOTP MFA API](/docs/reference/javascript/auth-mfa-api) is free to use and is enabled on all Supabase projects by default.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Add enrollment flow
|
||||
|
||||
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
|
||||
|
||||
1. Right after login or sign up.
|
||||
This lets users quickly set up MFA immediately after they log in or create an
|
||||
This lets users set up MFA immediately after they log in or create an
|
||||
account. We recommend encouraging all users to set up MFA if that makes sense
|
||||
for your application. Many applications offer this as an opt-in step in an
|
||||
effort to reduce onboarding friction.
|
||||
@@ -294,29 +311,6 @@ function AuthMFA() {
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light mt-8 mb-6 [&>div]:space-y-4"
|
||||
>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">What's inside the QR code?</span>}
|
||||
id="what-is-inside-the-qr-code"
|
||||
>
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
header={<span className="text-foreground">How long is the TOTP code valid for?</span>}
|
||||
id="how-long-is-the-totp-code-valid-for"
|
||||
>
|
||||
### How long is the TOTP code valid for?
|
||||
|
||||
In our TOTP implementation, each generated code remains valid for one interval, which spans 30 seconds. To account for minor time discrepancies, we allow for a one-interval clock skew. This ensures that users can successfully authenticate within this timeframe, even if there are slight variations in system clocks.
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
@@ -118,7 +118,7 @@ This includes:
|
||||
|
||||
**Have another SMTP service set up on stand-by.**
|
||||
|
||||
In case the primary SMTP service you're using is experiencing difficulty, or your account is under threat of being blocked due to spam, you have another service to quickly turn to.
|
||||
In case the primary SMTP service you're using is experiencing difficulty, or your account is under threat of being blocked due to spam, you have another service to turn to.
|
||||
|
||||
**Use consistent branding and focused content.**
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ subtitle: 'General configuration options for Supabase Auth'
|
||||
|
||||
This section covers the [general configuration options](/dashboard/project/_/auth) for Supabase Auth. If you are looking for another type of configuration, you may be interested in one of the following sections:
|
||||
|
||||
- [Policies](/dashboard/project/_/auth/policies) to manage Row Level Security policies for your tables.
|
||||
- [Policies](/dashboard/project/_/database/policies) to manage Row Level Security policies for your tables.
|
||||
- [Sign In / Providers](/dashboard/project/_/auth/providers) to configure authentication providers and login methods for your users.
|
||||
- [Third Party Auth](/dashboard/project/_/auth/third-party) to use third-party authentication (TPA) systems based on JWTs to access your project.
|
||||
- [Sessions](/dashboard/project/_/auth/sessions) to configure settings for user sessions and refresh tokens.
|
||||
|
||||
Loaded 100 of 2071 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user