Merge branch 'master' into studio-e2e-faster-tests
No files matched your search
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: telemetry-standards
|
||||
description: PostHog event tracking standards for Supabase Studio. Use when reviewing
|
||||
PRs for telemetry compliance or implementing new event tracking. Covers event naming,
|
||||
property conventions, approved patterns, and implementation guide.
|
||||
---
|
||||
|
||||
# Telemetry Standards for Supabase Studio
|
||||
|
||||
Standards for PostHog event tracking in `apps/studio/`. Apply these when
|
||||
reviewing PRs that touch tracking or when implementing new tracking.
|
||||
|
||||
## Event Naming
|
||||
|
||||
**Format:** `[object]_[verb]` in snake_case
|
||||
|
||||
**Approved verbs only:**
|
||||
opened, clicked, submitted, created, removed, updated, retrieved, intended, evaluated, added,
|
||||
enabled, disabled, copied, exposed, failed, converted
|
||||
|
||||
**Flag these:**
|
||||
- Unapproved verbs (saved, viewed, seen, pressed, etc.)
|
||||
- Wrong order: `click_product_card` → should be `product_card_clicked`
|
||||
- Wrong casing: `productCardClicked` → should be `product_card_clicked`
|
||||
|
||||
**Good examples:**
|
||||
- `product_card_clicked`
|
||||
- `backup_button_clicked`
|
||||
- `sql_query_submitted`
|
||||
|
||||
**Common mistakes with corrections:**
|
||||
- `database_saved` → `save_button_clicked` or `database_updated` (unapproved verb)
|
||||
- `click_backup_button` → `backup_button_clicked` (wrong order)
|
||||
- `dashboardViewed` → don't track passive views on page load
|
||||
- `component_rendered` → don't track — no user interaction
|
||||
|
||||
## Property Standards
|
||||
|
||||
**Casing:** camelCase preferred for new events. The codebase has existing snake_case properties (e.g., `schema_name`, `table_name`) — when adding properties to an existing event, match its established convention.
|
||||
|
||||
**Names must be self-explanatory:**
|
||||
- `{ productType: 'database', planTier: 'pro' }`
|
||||
- `{ assistantType: 'sql', suggestionType: 'optimization' }`
|
||||
|
||||
**Flag these:**
|
||||
- Generic names: `label`, `value`, `name`, `data`
|
||||
- PascalCase properties
|
||||
- Inconsistent names across similar events (e.g., `assistantType` in one event, `aiType` in a related event)
|
||||
- Mixing camelCase and snake_case within the same event
|
||||
|
||||
## What NOT to Track
|
||||
|
||||
- Passive views/renders on page load (`dashboard_viewed`, `sidebar_appeared`, `page_loaded`)
|
||||
- Component appearances without user interaction
|
||||
- Generic "viewed" or "seen" events — already captured by pageview events
|
||||
|
||||
**DO track:** user clicks, form submissions, explicit opens/closes, user-initiated actions.
|
||||
|
||||
**Exception:** `_exposed` events for A/B experiment exposure tracking are valid even though they fire on render.
|
||||
|
||||
**Never track PII** (emails, names, IPs, etc.) in event properties.
|
||||
|
||||
## Required Pattern
|
||||
|
||||
Import `useTrack` from `lib/telemetry/track` (within `apps/studio/`). Never use `useSendEventMutation` (deprecated).
|
||||
|
||||
```typescript
|
||||
import { useTrack } from 'lib/telemetry/track'
|
||||
|
||||
const MyComponent = () => {
|
||||
const track = useTrack()
|
||||
|
||||
const handleClick = () => {
|
||||
track('product_card_clicked', {
|
||||
productType: 'database',
|
||||
planTier: 'pro',
|
||||
source: 'dashboard',
|
||||
})
|
||||
}
|
||||
|
||||
return <button onClick={handleClick}>Click me</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Event Definitions
|
||||
|
||||
All events must be defined as TypeScript interfaces in `packages/common/telemetry-constants.ts`:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* [Event description]
|
||||
*
|
||||
* @group Events
|
||||
* @source [what triggers this event]
|
||||
*/
|
||||
export interface MyFeatureClickedEvent {
|
||||
action: 'my_feature_clicked'
|
||||
properties: {
|
||||
/** Description of property */
|
||||
featureType: string
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
```
|
||||
|
||||
Add the new interface to the `TelemetryEvent` union type so `useTrack` picks it up.
|
||||
`@group Events` and `@source` must be accurate.
|
||||
|
||||
## Review Rules
|
||||
|
||||
When reviewing a PR, flag these as **required changes:**
|
||||
|
||||
1. **Naming violations** — event not following `[object]_[verb]` snake_case, or using an unapproved verb
|
||||
2. **Property violations** — not camelCase, generic names, or inconsistent with similar events
|
||||
3. **Deprecated hook** — any usage of `useSendEventMutation` instead of `useTrack`
|
||||
4. **Unnecessary view tracking** — events that fire on page load without user interaction
|
||||
5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation
|
||||
|
||||
When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest:
|
||||
- "This adds a user interaction that may benefit from tracking."
|
||||
- Propose the event name following `[object]_[verb]` convention
|
||||
- Propose the `useTrack()` call with suggested properties
|
||||
|
||||
When checking property consistency, search `packages/common/telemetry-constants.ts` for similar events and verify property names match.
|
||||
|
||||
## Well-Formed Event Examples
|
||||
|
||||
From the actual codebase:
|
||||
|
||||
```typescript
|
||||
// User copies a connection string
|
||||
track('connection_string_copied', {
|
||||
connectionType: 'psql',
|
||||
connectionMethod: 'transaction_pooler',
|
||||
connectionTab: 'Connection String',
|
||||
})
|
||||
|
||||
// User enables a feature preview
|
||||
track('feature_preview_enabled', {
|
||||
feature: 'realtime_inspector',
|
||||
})
|
||||
|
||||
// User clicks a banner CTA
|
||||
track('index_advisor_banner_dismiss_button_clicked')
|
||||
|
||||
// Experiment exposure (fires on render — valid exception)
|
||||
track('home_new_experiment_exposed', {
|
||||
variant: 'treatment',
|
||||
})
|
||||
```
|
||||
|
||||
## Implementing New Tracking
|
||||
|
||||
To add tracking for a user action:
|
||||
|
||||
1. **Name the event** — `[object]_[verb]` using approved verbs only
|
||||
2. **Choose properties** — camelCase preferred for new events; check `packages/common/telemetry-constants.ts` for similar events and match their property names and casing
|
||||
3. **Add interface to telemetry-constants.ts** — with `@group Events` and `@source` JSDoc, add to the `TelemetryEvent` union type
|
||||
4. **Add to component** — `import { useTrack } from 'lib/telemetry/track'`, call `track('event_name', { properties })`
|
||||
|
||||
### Verification checklist
|
||||
|
||||
- [ ] Event name follows `[object]_[verb]` with approved verb
|
||||
- [ ] Event name is snake_case
|
||||
- [ ] Properties are camelCase and self-explanatory
|
||||
- [ ] Event defined in telemetry-constants.ts with accurate `@page`/`@source`
|
||||
- [ ] Using `useTrack` hook (not `useSendEventMutation`)
|
||||
- [ ] Not tracking passive views/appearances
|
||||
- [ ] No PII in event properties (emails, names, IPs, etc.)
|
||||
- [ ] Property names consistent with similar events
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
# fetch only the root files and scripts folder
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
apps
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
ref: master
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -31,6 +31,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -30,6 +30,7 @@ jobs:
|
||||
examples
|
||||
packages
|
||||
supabase
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
packages/pg-meta
|
||||
packages/tsconfig
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -20,7 +20,9 @@ jobs:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
sparse-checkout: apps
|
||||
sparse-checkout: |
|
||||
apps
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
@@ -46,6 +48,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
i18n
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
@@ -71,6 +74,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
apps/docs/pages
|
||||
apps/docs/content
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
|
||||
@@ -48,6 +48,7 @@ jobs:
|
||||
examples
|
||||
packages
|
||||
supabase
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
.github
|
||||
apps/studio
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
.github
|
||||
apps/studio
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
@@ -41,5 +42,5 @@ jobs:
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run ratchet script
|
||||
- name: Run ratchet script
|
||||
run: pnpm --filter studio run lint:ratchet
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
sparse-checkout: |
|
||||
apps/studio
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -83,6 +85,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/studio
|
||||
patches
|
||||
- name: Download coverage artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
|
||||
@@ -23,6 +23,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
if: steps.filter.outputs.relevant == 'true'
|
||||
|
||||
@@ -30,6 +30,7 @@ jobs:
|
||||
apps/www
|
||||
packages
|
||||
supabase
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -156,4 +156,7 @@ gcloud.json
|
||||
keys.json
|
||||
|
||||
# Playwright MCP
|
||||
.playwright-mcp/*
|
||||
.playwright-mcp/*
|
||||
|
||||
# Application
|
||||
**/.superset/**
|
||||
@@ -1292,6 +1292,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"sheet-confirm-on-close-demo": {
|
||||
name: "sheet-confirm-on-close-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["alert-dialog","button","input","label","separator","sheet"],
|
||||
component: React.lazy(() => import("@/registry/default/example/sheet-confirm-on-close-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/sheet-confirm-on-close-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"sheet-demo": {
|
||||
name: "sheet-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -99,6 +99,7 @@ This enforced decision helps prevent accidental dismissal of critical warnings o
|
||||
|
||||
- **Keep content concise:** AlertDialogDescription renders as a single paragraph and must not contain block-level elements such as lists, multiple paragraphs, or complex layouts.
|
||||
- **Use for critical decisions only:** Reserve Alert Dialog for destructive or irreversible actions, or for warnings that require explicit acknowledgement.
|
||||
- **Use for dirty-form discard confirmation:** A short discard-confirmation step after a dirty form dismissal attempt (backdrop, Escape, or `Cancel`) is a valid Alert Dialog pattern. In Studio, prefer `DiscardChangesConfirmationDialog` for this flow.
|
||||
- **Always provide a cancel action:** Include AlertDialogCancel so users can safely back out, in addition to supporting the Escape key.
|
||||
- **Avoid rich content:** If the dialog requires detailed explanations, callouts, or form inputs, use [Confirmation Modal](../fragments/confirmation-modal) or [Dialog](../components/dialog) instead.
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
|
||||
- **Use for non-critical interactions:** Dialog is appropriate when dismissal has no serious consequences.
|
||||
- **Design for cancellation**: Assume users may close the dialog without completing the action.
|
||||
- **Guard dirty forms on dismissal:** For form dialogs, keep `Dialog` dismissible but intercept close attempts when the form is dirty and show a discard-confirmation dialog (for example, Studio's `DiscardChangesConfirmationDialog` pattern via `useConfirmOnClose`).
|
||||
- **Keep focus contained**: Dialog content should remain scoped to a single task or flow.
|
||||
- **Avoid destructive confirmations**: If the dialog’s primary purpose is to confirm a risky action, use a confirmation-focused pattern instead.
|
||||
- **Compose freely**: Dialog is intentionally unopinionated. Build custom layouts, forms, or step-based flows as needed.
|
||||
|
||||
@@ -10,6 +10,8 @@ Use Confirmation Modal when the user needs extra context to make a decision, suc
|
||||
|
||||
If the confirmation can be expressed as a single short paragraph, use [Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See [Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern.
|
||||
|
||||
For dirty-form dismissal in dialogs/sheets, use the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) instead of `ConfirmationModal`. Avoid creating custom wrapper components for this flow; wire `modalProps` from `useConfirmOnClose` directly into `DiscardChangesConfirmationDialog`.
|
||||
|
||||
<ComponentPreview name="confirmation-modal-demo" peekCode wide />
|
||||
|
||||
## Usage
|
||||
@@ -54,6 +56,7 @@ export default function ConfirmationModalDemo() {
|
||||
## Guidelines
|
||||
|
||||
- **Use for moderate complexity:** Suitable when the confirmation requires more than a single sentence but does not need typed intent.
|
||||
- **Do not use for standard dirty-form dismissal:** Prefer `DiscardChangesConfirmationDialog` for unsaved-changes prompts so copy, behavior, and wiring stay consistent across dialogs/sheets.
|
||||
- **Avoid critical destruction:** Do not use for irreversible or high-risk actions that could benefit from stronger safeguards.
|
||||
- **Keep content focused:** Include only the context needed to make the decision. If the dialog becomes a full flow, use a custom [Dialog](../components/dialog) instead.
|
||||
- **Provide clear actions:** Ensure confirm and cancel labels clearly describe the outcome of each choice.
|
||||
|
||||
@@ -74,3 +74,53 @@ Sheets are dialogs presented as side panels. Use them for content that is larger
|
||||
[Sheet](../components/sheet) is modal by default, blocking interaction with the underlying page.
|
||||
|
||||
<ComponentPreview name="sheet-demo" />
|
||||
|
||||
## Best practices
|
||||
|
||||
### Dirty form dismissal
|
||||
|
||||
When a dialog or sheet contains a form, keep all normal dismissal affordances enabled (backdrop click, Escape key, close icon, and footer `Cancel` button).
|
||||
|
||||
Decision flow:
|
||||
|
||||
1. User attempts to close the dialog/sheet.
|
||||
2. If the form is clean, close immediately.
|
||||
3. If the form is dirty, show a discard-confirmation dialog.
|
||||
4. `Keep editing` returns to the form.
|
||||
5. `Discard changes` closes and resets the form.
|
||||
|
||||
Implementation checklist:
|
||||
|
||||
- Intercept close attempts from `onOpenChange`.
|
||||
- Route footer `Cancel` through the same close guard.
|
||||
- Render a separate discard confirmation dialog when dirty.
|
||||
- Keep `Cancel` non-destructive; use `Discard`/`Discard changes` for one-click destructive exits.
|
||||
- Guard controlled close attempts only; do not try to block route changes or arbitrary unmounts.
|
||||
|
||||
Studio implementation (preferred in Studio code):
|
||||
|
||||
```tsx
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
...
|
||||
<Button type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
...
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</Sheet>
|
||||
```
|
||||
|
||||
Generic implementation (outside Studio):
|
||||
|
||||
- If Studio-only helpers are unavailable, recreate the same behavior with `AlertDialog`.
|
||||
- The demo below shows the same flow and API shape (`confirmOnClose`, `handleOpenChange`, `modalProps`).
|
||||
|
||||
<ComponentPreview name="sheet-confirm-on-close-demo" />
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
Input_Shadcn_ as Input,
|
||||
Label_Shadcn_ as Label,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
|
||||
interface EndpointValues {
|
||||
endpointUrl: string
|
||||
secretHeader: string
|
||||
}
|
||||
|
||||
interface ConfirmOnCloseModalProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const defaultValues: EndpointValues = {
|
||||
endpointUrl: '',
|
||||
secretHeader: '',
|
||||
}
|
||||
|
||||
const useConfirmOnClose = ({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
}: {
|
||||
checkIsDirty: () => boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const confirmOnClose = useCallback(() => {
|
||||
if (checkIsDirty()) {
|
||||
setVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
onClose()
|
||||
}, [checkIsDirty, onClose])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
confirmOnClose()
|
||||
}
|
||||
},
|
||||
[confirmOnClose]
|
||||
)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
setVisible(false)
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
const modalProps: ConfirmOnCloseModalProps = useMemo(
|
||||
() => ({
|
||||
visible,
|
||||
onClose: onConfirm,
|
||||
onCancel,
|
||||
}),
|
||||
[visible, onConfirm, onCancel]
|
||||
)
|
||||
|
||||
return {
|
||||
confirmOnClose,
|
||||
handleOpenChange,
|
||||
modalProps,
|
||||
}
|
||||
}
|
||||
|
||||
const DiscardChangesAlertDialog = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => {
|
||||
const isConfirmingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
isConfirmingRef.current = false
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
isConfirmingRef.current = true
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) return
|
||||
|
||||
if (isConfirmingRef.current) {
|
||||
isConfirmingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
onCancel()
|
||||
},
|
||||
[onCancel]
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Any unsaved changes to this endpoint will be lost.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep editing</AlertDialogCancel>
|
||||
<AlertDialogAction variant="danger" onClick={handleConfirm}>
|
||||
Discard changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SheetConfirmOnCloseDemo() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [savedValues, setSavedValues] = useState<EndpointValues>(defaultValues)
|
||||
const [draftValues, setDraftValues] = useState<EndpointValues>(defaultValues)
|
||||
|
||||
const isDirty = useMemo(
|
||||
() =>
|
||||
draftValues.endpointUrl !== savedValues.endpointUrl ||
|
||||
draftValues.secretHeader !== savedValues.secretHeader,
|
||||
[draftValues, savedValues]
|
||||
)
|
||||
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setDraftValues(savedValues)
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const openSheet = () => {
|
||||
setDraftValues(savedValues)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
setSavedValues(draftValues)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="default" onClick={openSheet}>
|
||||
Open endpoint sheet
|
||||
</Button>
|
||||
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit endpoint</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Separator />
|
||||
<SheetSection className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endpoint-url">Endpoint URL</Label>
|
||||
<Input
|
||||
id="endpoint-url"
|
||||
value={draftValues.endpointUrl}
|
||||
placeholder="https://api.example.com/webhooks/supabase"
|
||||
onChange={(event) =>
|
||||
setDraftValues((current) => ({
|
||||
...current,
|
||||
endpointUrl: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secret-header">Secret header</Label>
|
||||
<Input
|
||||
id="secret-header"
|
||||
value={draftValues.secretHeader}
|
||||
placeholder="Bearer top-secret-value"
|
||||
onChange={(event) =>
|
||||
setDraftValues((current) => ({
|
||||
...current,
|
||||
secretHeader: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<SheetFooter>
|
||||
<Button type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={saveChanges} disabled={!isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<DiscardChangesAlertDialog {...modalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -761,6 +761,12 @@ export const examples: Registry = [
|
||||
registryDependencies: ['separator'],
|
||||
files: ['example/separator-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'sheet-confirm-on-close-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['alert-dialog', 'button', 'input', 'label', 'separator', 'sheet'],
|
||||
files: ['example/sheet-confirm-on-close-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'sheet-demo',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -30,6 +30,8 @@ public/sitemap.xml
|
||||
# llms.txt (generated)
|
||||
public/llms.txt
|
||||
public/llms/
|
||||
# Generated guide markdown files
|
||||
public/docs/
|
||||
|
||||
# Copied examples folder
|
||||
/examples/
|
||||
|
||||
@@ -22,6 +22,19 @@ For a complete run-down on how all of our tools work together, see the main DEVE
|
||||
4. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end
|
||||
5. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
## AI friendly documentation
|
||||
|
||||
This project generates Markdown files for each page under `/docs/guides/..` path.
|
||||
|
||||
To test locally, within the `apps/docs` directory:
|
||||
|
||||
1. Run `pnpm build:guides-markdown`
|
||||
2. Run `pnpm dev`
|
||||
|
||||
This creates Markdown files for all routes under the `public/docs/guides` directory, ignored by Git.
|
||||
|
||||
For production this setup runs as a `prebuild` task to allow Vercel to bundle these files with middleware and functions.
|
||||
|
||||
## Contributing
|
||||
|
||||
For repo organization and style guide, see the [contributing guide](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params
|
||||
const baseDir = path.join(process.cwd(), 'public/docs/guides')
|
||||
const filePath = path.join(baseDir, `${slug.join('/')}.md`)
|
||||
|
||||
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
return new NextResponse(content, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
})
|
||||
} catch {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { CodeBlock } from 'ui'
|
||||
import { Heading } from 'ui/src/components/CustomHTMLElements'
|
||||
import { type TOCHeader } from '~/components/GuidesTableOfContents'
|
||||
import { type TOCHeader } from '~/components/GuidesSidebar'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import type { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client'
|
||||
|
||||
import { Check, Copy, ExternalLink } from 'lucide-react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import { cn } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
|
||||
import { Feedback } from '~/components/Feedback'
|
||||
import { useTocAnchors } from '../features/docs/GuidesMdx.state'
|
||||
import { Chatgpt } from 'icons'
|
||||
import { Claude } from 'icons'
|
||||
|
||||
interface TOCHeader {
|
||||
id?: string
|
||||
text: string
|
||||
link: string
|
||||
level: number
|
||||
}
|
||||
|
||||
function AiTools({ className }: { className?: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const path = usePathname()
|
||||
|
||||
async function copyMarkdown() {
|
||||
const mdUrl = `/docs/${path}.md`
|
||||
|
||||
try {
|
||||
const res = await fetch(mdUrl)
|
||||
const text = await res.text()
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy markdown', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={cn(className)} aria-labelledby="ask-ai-title">
|
||||
<h3
|
||||
id="ask-ai-title"
|
||||
className="block font-mono uppercase text-xs text-foreground-light mb-3"
|
||||
>
|
||||
AI Tools
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={copyMarkdown}
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground text-left transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={14} strokeWidth={1.5} className="text-brand" />
|
||||
) : (
|
||||
<Copy size={14} strokeWidth={1.5} />
|
||||
)}
|
||||
{copied ? 'Copied!' : 'Copy as Markdown'}
|
||||
</button>
|
||||
<a
|
||||
href={`https://chatgpt.com/?hint=search&q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<Chatgpt size={14} strokeWidth={0} />
|
||||
Ask ChatGPT
|
||||
</a>
|
||||
<a
|
||||
href={`https://claude.ai/new?q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<Claude size={14} strokeWidth={0} />
|
||||
Ask Claude
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const GuidesSidebar = ({
|
||||
className,
|
||||
video,
|
||||
hideToc,
|
||||
}: {
|
||||
className?: string
|
||||
video?: string
|
||||
hideToc?: boolean
|
||||
}) => {
|
||||
const pathname = usePathname()
|
||||
const { toc } = useTocAnchors()
|
||||
const showFeedback = isFeatureEnabled('feedback:docs')
|
||||
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
return (
|
||||
<div className={cn('thin-scrollbar overflow-y-auto h-fit', 'px-px', className)}>
|
||||
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
|
||||
{video && (
|
||||
<div className="relative pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
{showFeedback && (
|
||||
<div className="pl-5">
|
||||
<Feedback key={pathname} />
|
||||
</div>
|
||||
)}
|
||||
<div className="pl-5">
|
||||
<AiTools key={pathname} />
|
||||
</div>
|
||||
{!hideToc && toc.length !== 0 && (
|
||||
<Toc className="-ml-[calc(0.25rem+6px)]">
|
||||
<h3 className="inline-flex items-center gap-1.5 font-mono text-xs uppercase text-foreground pl-[calc(1.5rem+6px)]">
|
||||
On this page
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
<TOCItems items={toc} />
|
||||
</TOCScrollArea>
|
||||
</Toc>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GuidesSidebar
|
||||
export { GuidesSidebar }
|
||||
export type { TOCHeader }
|
||||
@@ -1,55 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import { cn } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
|
||||
import { Feedback } from '~/components/Feedback'
|
||||
import { useTocAnchors } from '../features/docs/GuidesMdx.state'
|
||||
|
||||
interface TOCHeader {
|
||||
id?: string
|
||||
text: string
|
||||
link: string
|
||||
level: number
|
||||
}
|
||||
|
||||
const GuidesTableOfContents = ({ className, video }: { className?: string; video?: string }) => {
|
||||
const pathname = usePathname()
|
||||
const { toc } = useTocAnchors()
|
||||
|
||||
const showFeedback = isFeatureEnabled('feedback:docs')
|
||||
|
||||
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
return (
|
||||
<div className={cn('thin-scrollbar overflow-y-auto h-fit', 'px-px', className)}>
|
||||
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
|
||||
{video && (
|
||||
<div className="relative pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
{showFeedback && (
|
||||
<div className="pl-5">
|
||||
<Feedback key={pathname} />
|
||||
</div>
|
||||
)}
|
||||
{toc.length !== 0 && (
|
||||
<Toc className="-ml-[calc(0.25rem+6px)]">
|
||||
<h3 className="inline-flex items-center gap-1.5 font-mono text-xs uppercase text-foreground pl-[calc(1.5rem+6px)]">
|
||||
On this page
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
<TOCItems items={toc} />
|
||||
</TOCScrollArea>
|
||||
</Toc>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GuidesTableOfContents
|
||||
export type { TOCHeader }
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
// End of third-party imports
|
||||
|
||||
import { isFeatureEnabled } from 'common/enabled-features'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { IconPanel } from 'ui-patterns/IconPanel'
|
||||
|
||||
import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types'
|
||||
|
||||
const {
|
||||
@@ -2849,10 +2850,19 @@ export const self_hosting: NavMenuConstant = {
|
||||
name: 'How-to Guides',
|
||||
items: [
|
||||
{ name: 'Self-Hosted Functions', url: '/guides/self-hosting/self-hosted-functions' },
|
||||
{ name: 'Restore from Platform', url: '/guides/self-hosting/restore-from-platform' },
|
||||
{
|
||||
name: 'Add Reverse Proxy with HTTPS',
|
||||
url: '/guides/self-hosting/self-hosted-proxy-https',
|
||||
},
|
||||
{
|
||||
name: 'Restore Project from Platform',
|
||||
url: '/guides/self-hosting/restore-from-platform',
|
||||
},
|
||||
{ name: 'Configure S3 Storage', url: '/guides/self-hosting/self-hosted-s3' },
|
||||
{ name: 'Copy Storage from Platform', url: '/guides/self-hosting/copy-from-platform-s3' },
|
||||
{ name: 'Enabling MCP server', url: '/guides/self-hosting/enable-mcp' },
|
||||
{ name: 'Configure Social Login (OAuth)', url: '/guides/self-hosting/self-hosted-oauth' },
|
||||
{ name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' },
|
||||
{ name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,13 +6,15 @@ description: 'Supabase provides documentation that updates automatically.'
|
||||
|
||||
Supabase generates documentation in the [Dashboard](/dashboard) which updates as you make database changes.
|
||||
|
||||
1. Go to the [API](/dashboard/project/_/api) page in the Dashboard.
|
||||
2. Select any table under **Tables and Views** in the sidebar.
|
||||
3. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
1. Go to the [Project Settings](/dashboard/project/_/settings/general) page in the Dashboard.
|
||||
2. Select Data API -> Docs
|
||||
3. Select any table under **Tables and Views** in the sidebar.
|
||||
4. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
5. You may also select the SUPABASE_KEY to use.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs(2).mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
@@ -495,11 +495,11 @@ Store the client secret securely. It will only be shown once. If you lose it, yo
|
||||
|
||||
When a client exchanges an authorization code or refreshes a token, it must authenticate with the token endpoint. The `token_endpoint_auth_method` controls how this authentication happens:
|
||||
|
||||
| Method | Description | Used by |
|
||||
| --- | --- | --- |
|
||||
| `none` | No client authentication. Only `client_id` is sent in the request body. | Public clients (required) |
|
||||
| `client_secret_basic` | Client credentials sent via HTTP Basic auth (`Authorization: Basic <base64(client_id:client_secret)>`). **This is the default for confidential clients.** | Confidential clients |
|
||||
| `client_secret_post` | Client credentials sent in the request body (`client_id` and `client_secret` as form parameters). | Confidential clients |
|
||||
| Method | Description | Used by |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `none` | No client authentication. Only `client_id` is sent in the request body. | Public clients (required) |
|
||||
| `client_secret_basic` | Client credentials sent via HTTP Basic auth (`Authorization: Basic <base64(client_id:client_secret)>`). **This is the default for confidential clients.** | Confidential clients |
|
||||
| `client_secret_post` | Client credentials sent in the request body (`client_id` and `client_secret` as form parameters). | Confidential clients |
|
||||
|
||||
**Defaults:** Public clients default to `none`. Confidential clients default to `client_secret_basic` (per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591#section-2)).
|
||||
|
||||
|
||||
@@ -463,8 +463,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) =>
|
||||
headers.append('Set-Cookie', serializeCookieHeader(name, value))
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -497,8 +497,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) =>
|
||||
headers.append('Set-Cookie', serializeCookieHeader(name, value))
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th
|
||||
|
||||
### What are recommended values for access token (JWT) expiration?
|
||||
|
||||
Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/jwt) in the Advanced Settings section.
|
||||
Most applications should use the default expiration time of 1 hour. This can be customized in your [project's settings](/dashboard/project/_/settings/jwt/legacy) in the JWT Keys > Legacy JWT Secret section.
|
||||
|
||||
Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations.
|
||||
|
||||
|
||||
@@ -1,75 +1,39 @@
|
||||
---
|
||||
title: 'Database Backups'
|
||||
description: 'Learn about the available backup methods for your Supabase project.'
|
||||
description: 'Learn about backups for your Supabase project.'
|
||||
---
|
||||
|
||||
Database backups are an integral part of any disaster recovery plan. Disasters come in many shapes and sizes. It could be as simple as accidentally deleting a table column, the database crashing, or even a natural calamity wiping out the underlying hardware a database is running on. The risks and impact brought by these scenarios can never be fully eliminated, but only minimized or even mitigated. Having database backups is a form of insurance policy. They are essentially snapshots of the database at various points in time. When disaster strikes, database backups allow the project to be brought back to any of these points in time, therefore averting the crisis.
|
||||
We automatically back up all Free, Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard.
|
||||
|
||||
<Admonition type="note">
|
||||
Pro Plan projects can access the last 7 days of daily backups. Team Plan projects can access the last 14 days of daily backups, while Enterprise Plan projects can access up to 30 days of daily backups. If you need more frequent backups, consider enabling [Point-in-Time Recovery](#point-in-time-recovery). We recommend that free tier plan projects regularly export their data using the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump) and maintain off-site backups.
|
||||
|
||||
The Supabase team regularly monitors the status of backups. In case of any issues, you can [contact support](/dashboard/support/new). Also you can check out our [status page](https://status.supabase.com/) at any time.
|
||||
<Admonition type="caution">
|
||||
|
||||
When you delete a project, we permanently remove all associated data, including any backups stored in S3. This action is irreversible, so consider it carefully before proceeding.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
For security purposes, daily backups do not store passwords for custom roles, and you will not find them in downloadable files. If you restore from a daily backup and use custom roles, you will need to reset their passwords after the restoration completes.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Once a project is deleted all associated data will be permanently removed, including any backups stored in S3. This action is irreversible and should be carefully considered before proceeding.
|
||||
Database backups do not include objects you store via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects you deleted after that backup.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Types of backups
|
||||
## Backup and restore process
|
||||
|
||||
Database backups can be categorized into two types: **logical** and **physical**. You can learn more about them [here](/blog/postgresql-physical-logical-backups).
|
||||
You can access daily backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard and restore a project to any of the backups.
|
||||
|
||||
<Admonition type="note" label="Physical backups are not enabled by default">
|
||||
You can restore your project to any of the backups. To generate a logical backup yourself, use the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump).
|
||||
|
||||
To enable physical backups, you have three options:
|
||||
## Managing backups programmatically
|
||||
|
||||
- Enable [Point-in-Time Recovery (PITR)](#point-in-time-recovery)
|
||||
- [Increase your database size](/docs/guides/platform/database-size) to greater than 15GB
|
||||
- [Create a read replica](/docs/guides/platform/read-replicas)
|
||||
|
||||
Once a project satisfies at least one of the requirements for physical backups then logical backups are no longer made. However, your project may revert back to logical backups if you remove add-ons.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can confirm your project's backup type by navigating to [**Database Backups > Scheduled backups**](/dashboard/project/_/database/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical.
|
||||
|
||||
However, if your project has the Point-in-Time Recovery (PITR) add-on then the backups are physical and you can view them in [Database Backups > Point in time](/dashboard/project/_/database/backups/pitr).
|
||||
|
||||
## Frequency of backups
|
||||
|
||||
When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery (PITR). The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer granularity than Daily Backups, so it's unnecessary to run both.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Database backups do not include objects stored via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects that have been deleted since then.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Daily backups
|
||||
|
||||
All Pro, Team and Enterprise Plan Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For security purposes, passwords for custom roles are not stored in daily backups, and will not be found in downloadable files. As such, if you are restoring from a daily backup and are using custom roles, you will need to set their passwords once more following a completed restoration.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Backup process [#daily-backups-process]
|
||||
|
||||
The Postgres utility [pg_dumpall](https://www.postgresql.org/docs/current/app-pg-dumpall.html) is used to perform daily backups. An SQL file is generated, zipped up, and sent to our storage servers for safe keeping.
|
||||
|
||||
You can access daily backups in the [Scheduled backups](/dashboard/project/_/database/backups/scheduled) settings in the Dashboard. Pro Plan projects can access the last 7 days' worth of daily backups. Team Plan projects can access the last 14 days' worth of daily backups, while Enterprise Plan projects can access up to 30 days' worth of daily backups. Users can restore their project to any one of the backups. If you wish to generate a logical backup on your own, you can do so through the [Supabase CLI](/docs/reference/cli/supabase-db-dump).
|
||||
|
||||
You can also manage backups programmatically using the Management API:
|
||||
You can also manage backups programmatically [using the Management API](/docs/reference/api/v1-list-all-backups):
|
||||
|
||||
```bash
|
||||
# Get your access token from https://supabase.com/dashboard/account/tokens
|
||||
@@ -80,7 +44,7 @@ export PROJECT_REF="your-project-ref"
|
||||
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
||||
"https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups"
|
||||
|
||||
# Restore from a PITR (not logical) backup (replace ISO timestamp with desired restore point)
|
||||
# Restore from a PITR backup (replace Unix timestamp with desired restore point)
|
||||
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups/restore-pitr" \
|
||||
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -89,126 +53,87 @@ curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups
|
||||
}'
|
||||
```
|
||||
|
||||
#### Backup process for large databases
|
||||
### Restoration process
|
||||
|
||||
Databases larger than 15GB[^1], if they're on a recent build[^2] of the Supabase platform, get automatically transitioned[^3] to use daily physical backups. Physical backups are a more performant backup mechanism that lowers the overhead and impact on the database being backed up, and also avoids holding locks on objects in your database for a long period of time. While restores are unaffected, the backups created using this method cannot be downloaded from the Backups section of the dashboard.
|
||||
When selecting a backup to restore to, choose the closest available backup made before your desired restore point. You can always choose earlier backups, but consider how many days of data you might lose.
|
||||
|
||||
This class of physical backups only allows for recovery to a fixed time each day, similar to daily backups. You can upgrade to [PITR](#point-in-time-recovery) for access to more granular recovery options.
|
||||
The Dashboard prompts you for confirmation before proceeding with the restoration. The project is inaccessible during this process, so plan for downtime beforehand. Downtime depends on the size of the database—the larger it is, the longer the downtime will be.
|
||||
|
||||
Once a database is transitioned to using physical backups, it continues to use physical backups, even if the database size falls back below the threshold for the transition.
|
||||
After you confirm, we trigger the process to restore the desired backup data to your project. The dashboard will display a notification once the restoration completes.
|
||||
|
||||
[^1]: The threshold for transitioning will be slowly lowered over time. Eventually, all projects will be transitioned to using physical backups.
|
||||
[^2]: Projects created or upgraded after the 14th of July 2022 are eligible.
|
||||
[^3]: The transition to physical backups is handled transparently and does not require any user intervention. It involves a single restart of the database to pick up new configuration that can only be loaded at start; the expected downtime for the restart is a few seconds.
|
||||
|
||||
### Restoration process [#daily-backups-restoration-process]
|
||||
|
||||
When selecting a backup to restore to, select the closest available one made before the desired point in time to restore to. Earlier backups can always be chosen too but do consider the number of days' worth of data that could be lost.
|
||||
|
||||
The Dashboard will then prompt for a confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the underlying SQL of the chosen backup is then run against the project. The Postgres utility [psql](https://www.postgresql.org/docs/current/app-psql.html) is used to facilitate the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
|
||||
If your project is using subscriptions or replication slots, you will need to drop them prior to the restoration, and re-create them afterwards. The slot used by Realtime is exempted from this, and will be handled automatically.
|
||||
If your project uses subscriptions or replication slots, you need to drop them before the restoration and re-create them afterwards. We exempt the slot used by Realtime from this requirement and handle it automatically.
|
||||
|
||||
{/* screenshot of the Dashboard of the project completing restoration */}
|
||||
|
||||
## Point-in-Time recovery
|
||||
|
||||
Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. Even with daily backups, a day's worth of data could still be lost. With PITR, backups could be performed up to the point of disaster.
|
||||
Point-in-Time Recovery (PITR) allows you to back up a project at shorter intervals, giving you the option to restore to any chosen point with up to seconds of granularity. Even with daily backups, you could still lose a day's worth of data. With PITR, you can back up to the point of disaster.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Pro, Team and Enterprise Plan projects can enable PITR as an add-on.
|
||||
|
||||
Projects interested in PITR will also need to use at least a Small compute add-on, in order to ensure smooth functioning.
|
||||
Projects that want to use PITR must also use at least a Small compute add-on to ensure smooth functioning.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light mt-8 mb-6"
|
||||
>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="How PITR works"
|
||||
id="item-1"
|
||||
>
|
||||
|
||||
As [covered in this blog post](/blog/postgresql-physical-logical-backups), a combination of physical backups and [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) file archiving makes PITR possible. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change the database processes.
|
||||
|
||||
We use [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle both aspects of PITR. Daily, we take a snapshot of the database and send it to our storage servers. Throughout the day, as database transactions occur, we generate and upload WAL files.
|
||||
|
||||
By default, we back up WAL files at two-minute intervals. If these files exceed a certain file size threshold, we back them up immediately. During periods of high transaction volume, WAL file backups therefore become more frequent. Conversely, when the database has no activity, we do not make WAL file backups. Overall, in the worst case scenario, PITR achieves a Recovery Point Objective (RPO) of two minutes.
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer granularity than Daily Backups, so it's unnecessary to run both.
|
||||
If you enable PITR, we will no longer take Daily Backups. PITR provides finer granularity than Daily Backups, so running both is unnecessary.
|
||||
|
||||
</Admonition>
|
||||
|
||||
When you disable PITR, all new backups will still be taken as physical backups only. Physical backups can still be used for restoration, but they are not available for direct download. If you need to download a backup after PITR is disabled, you’ll need to take a manual [logical backup using the Supabase CLI or pg_dump](/docs/guides/platform/migrating-within-supabase/backup-restore#backup-database-using-the-cli).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If PITR has been disabled, logical backups remain available until they pass the backup retention period for your plan. After that window passes, only physical backups will be shown.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Backup process [#pitr-backup-process]
|
||||
|
||||
As discussed [here](/blog/postgresql-physical-logical-backups), PITR is made possible by a combination of taking physical backups of a project, as well as archiving [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) files. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change made in the database.
|
||||
|
||||
Supabase uses [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle both aspects of PITR. On a daily basis, a snapshot of the database is taken and sent to our storage servers. Throughout the day, as database transactions occur, WAL files are generated and uploaded.
|
||||
|
||||
By default, WAL files are backed up at two minute intervals. If these files cross a certain file size threshold, they are backed up immediately. As such, during periods of high amount of transactions, WAL file backups become more frequent. Conversely, when there is no activity in the database, WAL file backups are not made. Overall, this would mean that at the worst case scenario or disaster, the PITR achieves a Recovery Point Objective (RPO) of two minutes.
|
||||
### Backup process
|
||||
|
||||

|
||||
|
||||
You can access PITR in the [Point in Time](/dashboard/project/_/database/backups/pitr) settings in the Dashboard. The recovery period of a project is indicated by the earliest and latest points of recoveries displayed in your preferred timezone. If need be, the maximum amount of this recovery period can be modified accordingly.
|
||||
You can access PITR in the [Point in Time](/dashboard/project/_/database/backups/pitr) settings in the Dashboard. The recovery period of a project is shown by the earliest and latest recovery points displayed in your preferred timezone. You can change the maximum recovery period if needed.
|
||||
|
||||
Note that the latest restore point of the project could be significantly far from the current time. This occurs when there has not been any recent activity in the database, and therefore no WAL file backups have been made recently. This is perfectly fine as the state of the database at the latest point of recovery would still be indicative of the state of the database at the current time given that no transactions have been made in between.
|
||||
The latest restore point of the project could be significantly behind the current time. This occurs when the database has had no recent activity, and therefore we have not made any recent WAL file backups. However, the state of the database at the latest recovery point still reflects the current state of the database, given that no transactions have occurred in between.
|
||||
|
||||
### Restoration process [#pitr-restoration-process]
|
||||
### Restoration process
|
||||
|
||||

|
||||
|
||||
A date and time picker will be provided upon pressing the `Start a restore` button. The process will only proceed if the selected date and time fall within the earliest and latest points of recoveries.
|
||||
A date and time picker appears when you click the **Start a restore** button. The process only proceeds if the selected date and time fall within the earliest and latest recovery points.
|
||||
|
||||

|
||||
|
||||
After locking in the desired point in time to recover to, The Dashboard will then prompt for a review and confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot for downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the latest physical backup available is downloaded to the project and the database is partially restored. WAL files generated after this physical backup up to the specified point-in-time are then downloaded. The underlying records of transactions in these files are replayed against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
After selecting your desired recovery point, the Dashboard prompts you to review and confirm before proceeding with the restoration. The project is inaccessible during this process, so plan for downtime beforehand. Downtime depends on the size of the database—the larger it is, the longer the downtime will be. After you confirm, we download the latest available physical backup to the project and partially restore the database. We then download the WAL files generated after this physical backup up to your specified point in time. We replay the underlying transaction records in these files against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
|
||||
<$Show if="billing:all">
|
||||
<$Partial path="billing/pricing/pricing_pitr.mdx" />
|
||||
</$Show>
|
||||
|
||||
### Downloading backups after disabling PITR
|
||||
|
||||
When you disable PITR, we still take all new backups as physical backups only. You can still use physical backups for restoration, but they are not available for direct download. If you need to download a backup after disabling PITR, you need to take a manual [legacy logical backup using the Supabase CLI or pg_dump](/docs/guides/platform/migrating-within-supabase/backup-restore#backup-database-using-the-cli).
|
||||
|
||||
## Restore to a new project
|
||||
|
||||
See the [Duplicate Project docs](/docs/guides/platform/clone-project).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Logical backups
|
||||
|
||||
#### `search_path` issues
|
||||
|
||||
During the `pg_restore` process, the `search_path` is set to an empty string for predictability, and security. Using unqualified references to functions or relations can cause restorations using logical backups to fail, as the database will not be able to locate the function or relation being referenced. This can happen even if the database functions without issues during normal operations, as the `search_path` is usually set to include several schemas during normal operations. Therefore, you should always use schema-qualified names within your SQL code.
|
||||
|
||||
You can refer to [an example PR](https://github.com/supabase/supabase/pull/28393/files) on how to update SQL code to use schema-qualified names.
|
||||
|
||||
#### Invalid check constraints
|
||||
|
||||
Postgres requires that [check constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS) be:
|
||||
|
||||
1. immutable
|
||||
1. not reference table data other than the new or updated row being checked
|
||||
|
||||
Violating these requirements can result in numerous failure scenarios, including during logical restorations.
|
||||
|
||||
Common examples of check constraints that can result in such failures are:
|
||||
|
||||
- validating against the current time, e.g. that the row being inserted references a future event
|
||||
- validating the contents of a row against the contents of another table
|
||||
|
||||
#### Views that reference themselves
|
||||
|
||||
Views that directly or indirectly reference themselves will cause logical restores to fail due to cyclic dependency errors. These views are also invalid and unusable in Postgres, and any query against them will result in a runtime error.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
-- Direct self-reference
|
||||
CREATE VIEW my_view AS
|
||||
SELECT * FROM my_view;
|
||||
|
||||
-- Indirect circular reference
|
||||
CREATE VIEW v1 AS SELECT * FROM v2;
|
||||
CREATE VIEW v2 AS SELECT * FROM v1;
|
||||
```
|
||||
|
||||
-- Drop the offending view from your database, or delete them from the logical backup to make it restorable.
|
||||
|
||||
Postgres documentation [views](https://www.postgresql.org/docs/current/sql-createview.html)
|
||||
@@ -36,7 +36,7 @@ Usage is shown as "Log Drain Hours" on your invoice.
|
||||
|
||||
### Pricing
|
||||
|
||||
Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs <Price price="0.0822" /> per hour (<Price price="60" /> per month).
|
||||
Log Drains are available as a project Add-On for all Pro, Team and Enterprise users. Each Log Drain costs <Price price="0.0822" /> per hour (<Price price="60" /> per month).
|
||||
|
||||
## Log Drain Events
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ exists (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and topic = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('broadcast')
|
||||
)
|
||||
);
|
||||
@@ -266,7 +266,7 @@ with check (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and topic = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('broadcast')
|
||||
)
|
||||
);
|
||||
@@ -293,7 +293,7 @@ using (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and topic = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('presence')
|
||||
)
|
||||
);
|
||||
@@ -316,7 +316,7 @@ with check (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and name = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('presence')
|
||||
)
|
||||
);
|
||||
@@ -343,7 +343,7 @@ using (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and topic = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('broadcast', 'presence')
|
||||
)
|
||||
);
|
||||
@@ -366,7 +366,7 @@ with check (
|
||||
rooms_users
|
||||
where
|
||||
user_id = (select auth.uid())
|
||||
and name = (select realtime.topic())
|
||||
and room_topic = (select realtime.topic())
|
||||
and realtime.messages.extension in ('broadcast', 'presence')
|
||||
)
|
||||
);
|
||||
|
||||
@@ -428,6 +428,20 @@ By default all files are stored locally on the server. You can connect Storage t
|
||||
|
||||
See the [Configure S3 Storage](/docs/guides/self-hosting/self-hosted-s3) guide for detailed setup instructions.
|
||||
|
||||
#### Configuring HTTPS
|
||||
|
||||
By default, Supabase is accessible over HTTP. For production deployments, especially when using OAuth providers, you need HTTPS with a valid TLS certificate. The recommended approach is to place a reverse proxy (such as Caddy or Nginx) in front of Kong.
|
||||
|
||||
See the [Configure HTTPS](/docs/guides/self-hosting/self-hosted-proxy-https) guide for setup instructions.
|
||||
|
||||
#### Configuring social login (OAuth) providers
|
||||
|
||||
See the [Configure Social Login (OAuth) Providers](/docs/guides/self-hosting/self-hosted-oauth) guide for setup instructions.
|
||||
|
||||
#### Configuring phone login, SMS, and MFA
|
||||
|
||||
See the [Configure Phone Login & MFA](/docs/guides/self-hosting/self-hosted-phone-mfa) guide for SMS provider setup, OTP settings, and multi-factor authentication configuration.
|
||||
|
||||
#### Configuring Supabase AI Assistant
|
||||
|
||||
Configuring the Supabase AI Assistant is optional. By adding **your own** `OPENAI_API_KEY` to `.env` you can enable AI services, which help with writing SQL queries, statements, and policies.
|
||||
|
||||
@@ -125,14 +125,14 @@ const customVar = Deno.env.get('MY_CUSTOM_VAR')
|
||||
|
||||
The functions service is pre-configured with the following environment variables:
|
||||
|
||||
| Variable | Value | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `SUPABASE_URL` | `http://kong:8000` | Internal API gateway URL |
|
||||
| `SUPABASE_PUBLIC_URL` | `http://<your-domain>:8000` | Base URL for accessing Supabase from the Internet |
|
||||
| `JWT_SECRET` | Your secret key | Legacy symmetric encryption key used to sign and verify JWTs |
|
||||
| `SUPABASE_ANON_KEY` | Your anon key | Client-side API key with limited permissions (`anon` role). |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Your service role key | Server-side API key with full database access (`service_role` role) |
|
||||
| `SUPABASE_DB_URL` | Postgres connection string | Can be used for direct database access |
|
||||
| Variable | Value | Purpose |
|
||||
| --------------------------- | --------------------------- | ------------------------------------------------------------------- |
|
||||
| `SUPABASE_URL` | `http://kong:8000` | Internal API gateway URL |
|
||||
| `SUPABASE_PUBLIC_URL` | `http://<your-domain>:8000` | Base URL for accessing Supabase from the Internet |
|
||||
| `JWT_SECRET` | Your secret key | Legacy symmetric encryption key used to sign and verify JWTs |
|
||||
| `SUPABASE_ANON_KEY` | Your anon key | Client-side API key with limited permissions (`anon` role). |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Your service role key | Server-side API key with full database access (`service_role` role) |
|
||||
| `SUPABASE_DB_URL` | Postgres connection string | Can be used for direct database access |
|
||||
|
||||
Here's an example function that queries a table using `@supabase/supabase-js`:
|
||||
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
---
|
||||
title: 'Configure Social Login (OAuth) Providers'
|
||||
description: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
|
||||
subtitle: 'Set up social login (OAuth/OIDC) providers for self-hosted Supabase with Docker.'
|
||||
---
|
||||
|
||||
This guide covers the **server-side configuration** required to enable social login providers on a self-hosted Supabase instance running with Docker Compose. This applies to all OAuth and OIDC-based providers, including third-party identity providers like Keycloak.
|
||||
|
||||
## Before you begin
|
||||
|
||||
You need:
|
||||
|
||||
- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
|
||||
- `API_EXTERNAL_URL` set to the publicly reachable URL of your Supabase instance (e.g., `https://<your-domain>`).
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
HTTPS is strongly recommended in production. Most OAuth providers reject `http://` callback URLs (except `localhost`).
|
||||
|
||||
</Admonition>
|
||||
|
||||
Your **OAuth callback URL** is built from `API_EXTERNAL_URL`. For example, if `API_EXTERNAL_URL` is `https://<your-domain>`, the callback URL will become:
|
||||
|
||||
```
|
||||
https://<your-domain>/auth/v1/callback
|
||||
```
|
||||
|
||||
You will have to register this URL with each OAuth provider.
|
||||
|
||||
## OAuth request flow
|
||||
|
||||
When a user signs in with an OAuth provider, the following flow occurs:
|
||||
|
||||
1. Your app calls `supabase.auth.signInWithOAuth()` and the browser redirects to the Auth service
|
||||
2. API gateway (Kong) routes the request to the Auth container (`/auth/v1/authorize`)
|
||||
3. Auth redirects the user to the OAuth provider (e.g., Google) for consent
|
||||
4. The provider redirects back to `https://<your-domain>/auth/v1/callback`
|
||||
5. Auth exchanges the authorization code for tokens and redirects the user to your `SITE_URL` or an allowed redirect URL
|
||||
|
||||
## Auth environment variables
|
||||
|
||||
The Auth service (GoTrue) uses the prefix `GOTRUE_EXTERNAL_` followed by a provider name for all OAuth configuration. For example, when using Google:
|
||||
|
||||
- `GOTRUE_EXTERNAL_GOOGLE_ENABLED`
|
||||
- `GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID`
|
||||
- `GOTRUE_EXTERNAL_GOOGLE_SECRET`
|
||||
- `GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI`
|
||||
|
||||
## Step-by-step configuration
|
||||
|
||||
The default `.env.example` and `docker-compose.yml` include commented-out placeholders for Google, GitHub, and Azure.
|
||||
|
||||
### Step 1: Register your app with the provider
|
||||
|
||||
1. Go to the OAuth provider's developer console and create an application.
|
||||
2. Set the **authorized redirect URL**, e.g., `https://<your-domain>/auth/v1/callback`
|
||||
3. Copy the **client ID** and **client secret** into your `.env` file.
|
||||
|
||||
### Step 2: Configure variables in the `.env` file
|
||||
|
||||
Uncomment the lines for your provider in `.env` and add your client ID and secret, e.g., for Google:
|
||||
|
||||
```
|
||||
GOOGLE_ENABLED=true
|
||||
GOOGLE_CLIENT_ID=your-client-id
|
||||
GOOGLE_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
### Step 3: Enable the matching lines in `docker-compose.yml`
|
||||
|
||||
Uncomment the corresponding `GOTRUE_EXTERNAL_` lines in the `auth` service's `environment`:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
|
||||
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For providers **not** pre-configured in the files (see the [full provider list](#other-supported-providers) below), add the lines manually following the same pattern: variables in `.env`, passthrough with `GOTRUE_EXTERNAL_PROVIDER_` in `docker-compose.yml`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Step 4: Restart the auth service
|
||||
|
||||
```sh
|
||||
docker compose up -d --force-recreate --no-deps auth
|
||||
```
|
||||
|
||||
### Step 5: Verify the configuration
|
||||
|
||||
Check that the provider is enabled:
|
||||
|
||||
```sh
|
||||
curl -H 'apikey: your-anon-key' https://<your-domain>/auth/v1/settings
|
||||
```
|
||||
|
||||
The response should include your provider under `external`:
|
||||
|
||||
```
|
||||
{
|
||||
"external": {
|
||||
"google": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Provider-specific setup
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="google"
|
||||
>
|
||||
|
||||
<TabPanel id="google" label="Google">
|
||||
|
||||
**Google Cloud Console setup:**
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create or select a project
|
||||
3. Select **Solutions** > **All products** in the navigation menu on the left
|
||||
4. Go to **APIs & services** > **OAuth consent screen** and click **Get started**
|
||||
5. Follow the configuration steps and add an **External** app
|
||||
6. Go to **APIs & Services** > **Credentials**
|
||||
7. Click **Create Credentials** > **OAuth client ID**
|
||||
8. Set application type to **Web application**
|
||||
9. Under **Authorized redirect URIs**, add: `https://<your-domain>/auth/v1/callback`
|
||||
10. Click **Create** and copy the client ID and client secret
|
||||
|
||||
**`.env`:**
|
||||
|
||||
```
|
||||
GOOGLE_ENABLED=true
|
||||
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
|
||||
GOOGLE_SECRET=your-google-client-secret
|
||||
```
|
||||
|
||||
**`docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
|
||||
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="github" label="GitHub">
|
||||
|
||||
**GitHub setup:**
|
||||
|
||||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
2. Click **New OAuth app**
|
||||
3. Fill in **Homepage URL**, e.g., `https://<your-domain>`
|
||||
4. Set **Authorization callback URL** to: `https://<your-domain>/auth/v1/callback`
|
||||
5. Click **Register application**
|
||||
6. Copy the client ID, generate and copy a client secret
|
||||
|
||||
**`.env`:**
|
||||
|
||||
```
|
||||
GITHUB_ENABLED=true
|
||||
GITHUB_CLIENT_ID=your-github-client-id
|
||||
GITHUB_SECRET=your-github-client-secret
|
||||
```
|
||||
|
||||
**`docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
|
||||
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
|
||||
GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="azure" label="Azure">
|
||||
|
||||
**Azure Portal setup:**
|
||||
|
||||
1. Go to [Azure Portal](https://portal.azure.com/)
|
||||
2. Go to **All services** > **Identity** > **App registrations** via the navigation menu on the left
|
||||
3. Click **New registration**
|
||||
4. Add application **Name**
|
||||
5. Under **Redirect URI**, select **Web** and enter: `https://<your-domain>/auth/v1/callback`
|
||||
6. Click **Register**
|
||||
7. Copy the **Application (client) ID**
|
||||
8. Click on **Client credentials** > **Add a certificate or secret**
|
||||
9. Click on **New client secret** and add a client secret
|
||||
10. Copy the secret value (not "secret ID")
|
||||
|
||||
**`.env`:**
|
||||
|
||||
```
|
||||
AZURE_ENABLED=true
|
||||
AZURE_CLIENT_ID=your-azure-application-client-id
|
||||
AZURE_SECRET=your-azure-client-secret
|
||||
## Optional: restrict to a specific tenant (defaults to 'common')
|
||||
# AZURE_URL=https://login.microsoftonline.com/your-tenant-id
|
||||
```
|
||||
|
||||
**`docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
|
||||
GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
||||
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
## Optional: uncomment for tenant-specific Azure login
|
||||
# GOTRUE_EXTERNAL_AZURE_URL: ${AZURE_URL}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="apple" label="Apple">
|
||||
|
||||
**Apple Developer setup:**
|
||||
|
||||
1. Refer to [Apple Developer documentation](https://developer.apple.com/documentation/signinwithapple/configuring-your-environment-for-sign-in-with-apple) to learn how to enable App ID and create a Services ID
|
||||
2. Create a private key for sign in with Apple
|
||||
3. Generate a client secret JWT from your private key. See [Apple Developer documentation](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret) for details.
|
||||
|
||||
**`.env`:**
|
||||
|
||||
```
|
||||
APPLE_ENABLED=true
|
||||
APPLE_CLIENT_ID=com.example.your-services-id
|
||||
APPLE_SECRET=your-generated-jwt-client-secret
|
||||
```
|
||||
|
||||
**`docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_APPLE_ENABLED: ${APPLE_ENABLED}
|
||||
GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${APPLE_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_APPLE_SECRET: ${APPLE_SECRET}
|
||||
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Apple uses `response_mode=form_post` for its OAuth flow. The Auth service handles this automatically - no additional configuration is needed.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="keycloak" label="Keycloak">
|
||||
|
||||
**Keycloak setup:**
|
||||
|
||||
1. Open your Keycloak admin console
|
||||
2. Select (or create) the realm you want to use
|
||||
3. Go to **Clients** > **Create client**
|
||||
4. Set **Client type** to **OpenID Connect**
|
||||
5. Set **Client ID** (e.g., `supabase`)
|
||||
6. On the next screen, enable **Client authentication**
|
||||
7. Under **Valid redirect URIs**, add: `https://<your-domain>/auth/v1/callback`
|
||||
8. Save, then go to the **Credentials** tab and copy the **Client secret**
|
||||
|
||||
**`.env` variables:**
|
||||
|
||||
```
|
||||
KEYCLOAK_ENABLED=true
|
||||
KEYCLOAK_CLIENT_ID=supabase
|
||||
KEYCLOAK_SECRET=your-keycloak-client-secret
|
||||
## Required: your Keycloak realm URL
|
||||
KEYCLOAK_URL=https://keycloak.example.com/realms/myrealm
|
||||
```
|
||||
|
||||
**`docker-compose.yml` passthrough:**
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_EXTERNAL_KEYCLOAK_ENABLED: ${KEYCLOAK_ENABLED}
|
||||
GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_SECRET}
|
||||
GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
GOTRUE_EXTERNAL_KEYCLOAK_URL: ${KEYCLOAK_URL}
|
||||
```
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
`KEYCLOAK_URL` is **required**. It must be the full realm URL (e.g., `https://keycloak.example.com/realms/myrealm`). The Auth service uses this to discover the OIDC endpoints (`.well-known/openid-configuration`). Without it, Keycloak login will not work.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Other supported providers
|
||||
|
||||
Supabase Auth supports the following OAuth providers:
|
||||
|
||||
| Provider | Env prefix | Additional variables | Docs |
|
||||
| ----------------- | ---------------- | ------------------------------- | --------------------------------------------------------------------- |
|
||||
| Apple | `APPLE_` | - | [Login with Apple](/docs/guides/auth/social-login/auth-apple) |
|
||||
| Azure (Microsoft) | `AZURE_` | `URL` (tenant URL) | [Login with Azure](/docs/guides/auth/social-login/auth-azure) |
|
||||
| Bitbucket | `BITBUCKET_` | - | [Login with Bitbucket](/docs/guides/auth/social-login/auth-bitbucket) |
|
||||
| Discord | `DISCORD_` | - | [Login with Discord](/docs/guides/auth/social-login/auth-discord) |
|
||||
| Facebook | `FACEBOOK_` | - | [Login with Facebook](/docs/guides/auth/social-login/auth-facebook) |
|
||||
| Figma | `FIGMA_` | - | [Login with Figma](/docs/guides/auth/social-login/auth-figma) |
|
||||
| GitHub | `GITHUB_` | `URL` (for GitHub Enterprise) | [Login with GitHub](/docs/guides/auth/social-login/auth-github) |
|
||||
| GitLab | `GITLAB_` | `URL` (for self-hosted GitLab) | [Login with GitLab](/docs/guides/auth/social-login/auth-gitlab) |
|
||||
| Google | `GOOGLE_` | - | [Login with Google](/docs/guides/auth/social-login/auth-google) |
|
||||
| Kakao | `KAKAO_` | - | [Login with Kakao](/docs/guides/auth/social-login/auth-kakao) |
|
||||
| Keycloak (OIDC) | `KEYCLOAK_` | `URL` (realm URL, **required**) | [Login with Keycloak](/docs/guides/auth/social-login/auth-keycloak) |
|
||||
| LinkedIn (OIDC) | `LINKEDIN_OIDC_` | - | [Login with LinkedIn](/docs/guides/auth/social-login/auth-linkedin) |
|
||||
| Notion | `NOTION_` | - | [Login with Notion](/docs/guides/auth/social-login/auth-notion) |
|
||||
| Slack (OIDC) | `SLACK_OIDC_` | - | [Login with Slack](/docs/guides/auth/social-login/auth-slack) |
|
||||
| Snapchat | `SNAPCHAT_` | - | - |
|
||||
| Spotify | `SPOTIFY_` | - | [Login with Spotify](/docs/guides/auth/social-login/auth-spotify) |
|
||||
| Twitch | `TWITCH_` | - | [Login with Twitch](/docs/guides/auth/social-login/auth-twitch) |
|
||||
| Twitter | `TWITTER_` | - | [Login with Twitter](/docs/guides/auth/social-login/auth-twitter) |
|
||||
| WorkOS | `WORKOS_` | - | [Login with WorkOS](/docs/guides/auth/social-login/auth-workos) |
|
||||
| Zoom | `ZOOM_` | - | [Login with Zoom](/docs/guides/auth/social-login/auth-zoom) |
|
||||
|
||||
For each provider, you need at minimum `ENABLED`, `CLIENT_ID`, `SECRET`, and `REDIRECT_URI` in `.env` and `docker-compose.yml`.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
**LinkedIn (OIDC)** and **Slack (OIDC)** use multi-word env prefixes. The full Docker Compose variables are `GOTRUE_EXTERNAL_LINKEDIN_OIDC_CLIENT_ID` and `GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID` respectively - not `LINKEDIN_CLIENT_ID` or `SLACK_CLIENT_ID`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Test the login flow
|
||||
|
||||
You can test OAuth with the following minimal HTML page:
|
||||
|
||||
- Save the code below to `index.html`
|
||||
- Start `python -m http.server 3000` in the same directory
|
||||
- Make sure `SITE_URL` is set to `http://localhost:3000` in your self-hosted Supabase `.env` configuration
|
||||
- Open your browser and go to `http://localhost:3000`
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Supabase OAuth Test</h1>
|
||||
<button id="loginBtn">Sign in with Google</button>
|
||||
<pre id="result"></pre>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const SUPABASE_URL = 'https://<your-domain>'
|
||||
const SUPABASE_ANON_KEY = 'your-anon-key'
|
||||
|
||||
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
const button = document.getElementById('loginBtn')
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
})
|
||||
|
||||
if (error) {
|
||||
document.getElementById('result').textContent = JSON.stringify(error, null, 2)
|
||||
}
|
||||
})
|
||||
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
if (data.session) {
|
||||
document.getElementById('result').textContent =
|
||||
'Logged in as: ' + data.session.user.email
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
For detailed client-side integration, see [Social Login](/docs/guides/auth/social-login).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Provider not enabled" or provider shows `false` in `/auth/v1/settings`
|
||||
|
||||
- Check that `GOTRUE_EXTERNAL_*_ENABLED` is set to `true` in `docker-compose.yml`
|
||||
- Verify the `.env` variable is not empty, e.g., check with `docker compose exec auth env | grep GOOGLE`
|
||||
|
||||
### Variables added to `.env` but provider still not working
|
||||
|
||||
Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
|
||||
|
||||
```
|
||||
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
```
|
||||
|
||||
Run `docker compose exec auth env | grep GOTRUE_EXTERNAL` to verify the variables are reaching the container.
|
||||
|
||||
### `SITE_URL` or redirect URL errors after login
|
||||
|
||||
After a successful OAuth login, the Auth service redirects to `SITE_URL` or a URL from `ADDITIONAL_REDIRECT_URLS`. Ensure:
|
||||
|
||||
- `SITE_URL` in `.env` is set to your application's URL
|
||||
- If your app uses a different redirect URL, add it to `ADDITIONAL_REDIRECT_URLS` (comma-separated)
|
||||
|
||||
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
|
||||
|
||||
### Nonce check failure on mobile (Google Sign In)
|
||||
|
||||
When using Google Sign In on mobile with ID tokens, nonce verification may fail because mobile SDKs don't always support the nonce flow that the Auth service expects.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
`GOTRUE_EXTERNAL_SKIP_NONCE_CHECK` disables nonce validation on ID tokens, which weakens replay-attack protection. Treat it as a **short-lived troubleshooting workaround**, not a permanent fix:
|
||||
|
||||
- Enable it only in the environment where you're debugging the issue.
|
||||
- Revert it as soon as the issue is resolved.
|
||||
- Prefer fixing the client-side nonce handling or switching to an OAuth flow (authorization code with PKCE) that avoids ID-token nonce issues entirely.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To enable it, uncomment the following line in `docker-compose.yml`:
|
||||
|
||||
```
|
||||
GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true
|
||||
```
|
||||
|
||||
### Auth service fails to start
|
||||
|
||||
Check the auth container logs:
|
||||
|
||||
```sh
|
||||
docker compose logs auth
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
- Missing required environment variable (e.g., `CLIENT_ID` or `SECRET` is empty)
|
||||
- Invalid `API_EXTERNAL_URL` (must be a valid URL with protocol)
|
||||
|
||||
## Environment variable reference
|
||||
|
||||
All OAuth-related environment variables for the `auth` service in `docker-compose.yml`:
|
||||
|
||||
| Variable | Description | Required |
|
||||
| -------------------------------- | ------------------------------------------------------------------------ | -------- |
|
||||
| `GOTRUE_EXTERNAL_*_ENABLED` | Enable the provider (`true`/`false`) | Yes |
|
||||
| `GOTRUE_EXTERNAL_*_CLIENT_ID` | OAuth client ID from the provider | Yes |
|
||||
| `GOTRUE_EXTERNAL_*_SECRET` | OAuth client secret from the provider | Yes |
|
||||
| `GOTRUE_EXTERNAL_*_REDIRECT_URI` | Callback URL: `${API_EXTERNAL_URL}/auth/v1/callback` | Yes |
|
||||
| `GOTRUE_SITE_URL` | Default redirect URL after authentication (set via `SITE_URL` in `.env`) | Yes |
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Redirect URLs](/docs/guides/auth/redirect-urls)
|
||||
- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: 'Configure Phone Login & MFA'
|
||||
description: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
|
||||
subtitle: 'Set up phone login SMS providers, OTP settings, and multi-factor authentication for self-hosted Supabase with Docker.'
|
||||
---
|
||||
|
||||
This guide covers the **server-side configuration** for phone login and multi-factor authentication (MFA) on a self-hosted Supabase instance running with Docker Compose.
|
||||
|
||||
For client-side implementation, see [Phone Login](/docs/guides/auth/phone-login) and [Multi-Factor Authentication](/docs/guides/auth/auth-mfa).
|
||||
|
||||
## Before you begin
|
||||
|
||||
You need:
|
||||
|
||||
- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
|
||||
- An account with an SMS provider (e.g., Twilio)
|
||||
|
||||
Phone auth is **enabled by default** in the Docker setup (`ENABLE_PHONE_SIGNUP=true` in `.env`). However, without an SMS provider configured, the Auth service has no way to deliver OTP codes.
|
||||
|
||||
## SMS provider configuration
|
||||
|
||||
The default `.env.example` and `docker-compose.yml` include commented-out SMS provider placeholders. The example below uses Twilio - you'll need a Twilio account with an account SID, auth token, and message service SID. See [Twilio's documentation](https://www.twilio.com/docs/messaging) for how to obtain these credentials.
|
||||
|
||||
To enable SMS delivery:
|
||||
|
||||
### Step 1: Uncomment and configure the settings in `.env`
|
||||
|
||||
```
|
||||
SMS_PROVIDER=twilio
|
||||
SMS_OTP_EXP=60
|
||||
SMS_OTP_LENGTH=6
|
||||
SMS_MAX_FREQUENCY=60s
|
||||
SMS_TEMPLATE=Your code is {{ .Code }}
|
||||
|
||||
## Twilio credentials
|
||||
SMS_TWILIO_ACCOUNT_SID=your-account-sid
|
||||
SMS_TWILIO_AUTH_TOKEN=your-auth-token
|
||||
SMS_TWILIO_MESSAGE_SERVICE_SID=your-message-service-sid
|
||||
```
|
||||
|
||||
### Step 2: Uncomment the matching lines in `docker-compose.yml`
|
||||
|
||||
Uncomment the `GOTRUE_SMS_*` lines in the `auth` service's `environment` block:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment:
|
||||
# ... existing variables ...
|
||||
GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
|
||||
GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
|
||||
GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
|
||||
GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
|
||||
GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
|
||||
GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
|
||||
GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
|
||||
GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
|
||||
```
|
||||
|
||||
### Step 3: Restart the auth service
|
||||
|
||||
```sh
|
||||
docker compose up -d --force-recreate --no-deps auth
|
||||
```
|
||||
|
||||
### Step 4: Verify
|
||||
|
||||
```sh
|
||||
docker compose exec auth env | grep GOTRUE_SMS
|
||||
```
|
||||
|
||||
Confirm your provider and credentials appear in the output.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For providers other than Twilio, add the provider-specific `GOTRUE_SMS_*` lines manually to `docker-compose.yml`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## OTP settings
|
||||
|
||||
### Expiration
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
The default OTP expiration is **60 seconds**. This is often too short for production use, consider increasing it.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Set `SMS_OTP_EXP` in `.env` (value is in seconds):
|
||||
|
||||
```
|
||||
# Set expiration to 5 minutes
|
||||
SMS_OTP_EXP=300
|
||||
```
|
||||
|
||||
And ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`.
|
||||
|
||||
### Length
|
||||
|
||||
The default OTP length is 6 digits. You can set it to any value between 6 and 10:
|
||||
|
||||
```
|
||||
SMS_OTP_LENGTH=8
|
||||
```
|
||||
|
||||
### Rate limiting
|
||||
|
||||
`SMS_MAX_FREQUENCY` controls the minimum interval between SMS sends to the same phone number. The default is 60 seconds:
|
||||
|
||||
```
|
||||
## Allow one SMS every 30 seconds
|
||||
SMS_MAX_FREQUENCY=30s
|
||||
```
|
||||
|
||||
## Test OTPs for development
|
||||
|
||||
To avoid sending real SMS during development, use `SMS_TEST_OTP` to map phone numbers to fixed OTP codes:
|
||||
|
||||
```
|
||||
SMS_TEST_OTP=16505551234:123456,16505555678:654321
|
||||
```
|
||||
|
||||
And uncomment `GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}` in `docker-compose.yml`.
|
||||
|
||||
When a test phone number requests an OTP, the Auth service skips SMS delivery and accepts only the mapped code. Other phone numbers continue to use the real SMS provider.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Remove test OTPs before deploying to production. You can also set an expiration with `SMS_TEST_OTP_VALID_UNTIL` (ISO 8601 datetime, e.g., `2026-12-31T23:59:59Z`) so they stop working automatically.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Multi-factor authentication (MFA)
|
||||
|
||||
The Auth service supports three MFA factor types. Configure them by uncommenting variables in `.env` and the matching `GOTRUE_MFA_*` lines in `docker-compose.yml`.
|
||||
|
||||
### App authenticator (TOTP)
|
||||
|
||||
TOTP is **enabled by default** - users can enroll with apps like Google Authenticator or Authy without any additional configuration.
|
||||
|
||||
To disable TOTP:
|
||||
|
||||
```
|
||||
MFA_TOTP_ENROLL_ENABLED=false
|
||||
MFA_TOTP_VERIFY_ENABLED=false
|
||||
```
|
||||
|
||||
### Phone MFA
|
||||
|
||||
Phone MFA is **disabled by default** (opt-in). It uses the same SMS provider configuration as phone login.
|
||||
|
||||
To enable:
|
||||
|
||||
```
|
||||
MFA_PHONE_ENROLL_ENABLED=true
|
||||
MFA_PHONE_VERIFY_ENABLED=true
|
||||
```
|
||||
|
||||
### Maximum enrolled factors
|
||||
|
||||
By default, a user can enroll up to 10 MFA factors. To change this:
|
||||
|
||||
```
|
||||
MFA_MAX_ENROLLED_FACTORS=5
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OTP expires too quickly
|
||||
|
||||
The default `SMS_OTP_EXP` is 60 seconds. Increase it in `.env`:
|
||||
|
||||
```
|
||||
SMS_OTP_EXP=300
|
||||
```
|
||||
|
||||
Ensure `GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}` is uncommented in `docker-compose.yml`, then restart:
|
||||
|
||||
```sh
|
||||
docker compose up -d --force-recreate --no-deps auth
|
||||
```
|
||||
|
||||
### SMS not being delivered
|
||||
|
||||
Check the auth container logs for errors:
|
||||
|
||||
```sh
|
||||
docker compose logs auth --tail 50
|
||||
```
|
||||
|
||||
Verify provider credentials reach the container:
|
||||
|
||||
```sh
|
||||
docker compose exec auth env | grep GOTRUE_SMS
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
- Provider credentials are in `.env` but the matching `GOTRUE_SMS_*` line is still commented out in `docker-compose.yml`
|
||||
- Provider credentials are wrong
|
||||
- Phone number format is wrong (use E.164 format: `+1234567890`)
|
||||
|
||||
### Variables are configured in `.env` but not working
|
||||
|
||||
Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
|
||||
|
||||
```sh
|
||||
docker compose exec auth env | grep -E 'GOTRUE_SMS|GOTRUE_MFA'
|
||||
```
|
||||
|
||||
After changing the configuration environment variables, recreate the Auth service container:
|
||||
|
||||
```sh
|
||||
docker compose up -d --force-recreate --no-deps auth
|
||||
```
|
||||
|
||||
### Rate limit errors
|
||||
|
||||
If users see "rate limit exceeded" errors, check `SMS_MAX_FREQUENCY` (minimum interval between sends) and the global rate limit `GOTRUE_RATE_LIMIT_SMS_SENT` (default: 30 per hour).
|
||||
|
||||
### Additional resources
|
||||
|
||||
- [Multi-Factor Authentication (Phone)](/docs/guides/auth/auth-mfa/phone)
|
||||
- [Multi-Factor Authentication (TOTP)](/docs/guides/auth/auth-mfa/totp)
|
||||
- [Auth server on GitHub](https://github.com/supabase/auth) (check README and `example.env`)
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: 'Configure Reverse Proxy and HTTPS'
|
||||
description: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
|
||||
subtitle: 'Set up a reverse proxy with HTTPS for self-hosted Supabase.'
|
||||
---
|
||||
|
||||
HTTPS is required for production self-hosted Supabase deployments. This guide covers two production approaches using a reverse proxy in front of self-hosted Supabase API gateway, plus a self-signed certificate option for development environment.
|
||||
|
||||
## Before you begin
|
||||
|
||||
You need:
|
||||
|
||||
- A working self-hosted Supabase installation. See [Self-Hosting with Docker](/docs/guides/self-hosting/docker).
|
||||
- A domain name with DNS pointing to your server's public IP address (to obtain Let's Encrypt certificate).
|
||||
- Ports 80 and 443 open.
|
||||
|
||||
## Set up HTTPS
|
||||
|
||||
Below are two options for adding a reverse proxy with automatic HTTPS in front of your self-hosted Supabase: **Caddy** (simpler, zero-config TLS) and **Nginx + Let's Encrypt** (more control over proxy settings). Both sit in front of Kong and terminate TLS, so internal traffic stays on HTTP.
|
||||
|
||||
<Admonition type="tip" label="Using a different reverse proxy?">
|
||||
|
||||
If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefik.io/), [Nginx Proxy Manager](https://nginxproxymanager.com/), or another reverse proxy for your infrastructure, you can use it instead of Caddy or Nginx above. The key requirements are:
|
||||
|
||||
- Proxy to Kong on port `8000` (or `<your-ip>:8000` if the proxy runs outside the Docker network)
|
||||
- Enable WebSocket support (required for Realtime)
|
||||
- Proxy traffic to Storage directly to the container, bypassing Kong
|
||||
- Add `X-Forwarded` headers to all requests
|
||||
- Comment out Kong's host port bindings in `docker-compose.yml` if the proxy runs in the same Docker network
|
||||
- Update `SUPABASE_PUBLIC_URL`, `API_EXTERNAL_URL`, and `SITE_URL` in `.env` to your HTTPS URL
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Step 1: Remove public port bindings for API gateway
|
||||
|
||||
Comment out Kong's host port mappings in `docker-compose.yml` so that it's not exposed to the Internet:
|
||||
|
||||
```yaml
|
||||
kong:
|
||||
# ...
|
||||
ports:
|
||||
# - ${KONG_HTTP_PORT}:8000/tcp
|
||||
# - ${KONG_HTTPS_PORT}:8443/tcp
|
||||
```
|
||||
|
||||
Kong remains accessible to other containers on the internal Docker network.
|
||||
|
||||
### Step 2: Update environment variables
|
||||
|
||||
Update the URL configuration in your `.env` file to use your HTTPS domain:
|
||||
|
||||
```
|
||||
SUPABASE_PUBLIC_URL=https://<your-domain>
|
||||
API_EXTERNAL_URL=https://<your-domain>
|
||||
SITE_URL=https://<your-domain>
|
||||
```
|
||||
|
||||
Change the following to your domain name and a **valid** email address:
|
||||
|
||||
```
|
||||
PROXY_DOMAIN=your-domain.example.com
|
||||
CERTBOT_EMAIL=admin@your-domain.example.com
|
||||
```
|
||||
|
||||
### Step 3: Start the reverse proxy
|
||||
|
||||
Pick one of the options below and use the corresponding Docker Compose overlay.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="caddy"
|
||||
|
||||
> <TabPanel id="caddy" label="Caddy (easiest)">
|
||||
|
||||
[Caddy](https://caddyserver.com/) automatically provisions and renews Let's Encrypt TLS certificates with zero configuration. It also handles HTTP-to-HTTPS redirects, WebSocket upgrades, and HTTP/2 and HTTP/3 out of the box.
|
||||
|
||||
Start Caddy by using the pre-configured `docker-compose.caddy.yml` overlay:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
```
|
||||
|
||||
Caddy configuration is in `volumes/proxy/caddy/Caddyfile`.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="nginx" label="Nginx + Let's Encrypt">
|
||||
|
||||
This option uses a 3rd party Nginx Docker image ([`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot)), which includes Certbot for automatic Let's Encrypt certificate issuance and renewal in a single container.
|
||||
|
||||
Start Nginx by using the pre-configured `docker-compose.nginx.yml` overlay:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
```
|
||||
|
||||
Nginx configuration template is in `volumes/proxy/nginx/supabase-nginx.conf.tpl`. On container startup, `${NGINX_SERVER_NAME}` is substituted using the environment variable from the `.env` file. The [`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot) image reads the resolved `server_name` to determine which domain to request a Let's Encrypt certificate for.
|
||||
|
||||
HTTP-to-HTTPS redirects are handled automatically by the `jonasal/nginx-certbot` image.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Step 4: Verify HTTPS connection
|
||||
|
||||
```sh
|
||||
curl -I https://<your-domain>/auth/v1/
|
||||
```
|
||||
|
||||
You should receive a `401` response confirming you could connect to Auth.
|
||||
|
||||
## Self-signed certificates (development only)
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Self-signed certificates trigger browser warnings and are rejected by most OAuth providers. Use this approach only in development environment or internal networks.
|
||||
|
||||
</Admonition>
|
||||
|
||||
For development or internal networks where you cannot use Let's Encrypt, you can configure Kong to serve HTTPS directly using self-signed certificates.
|
||||
|
||||
### Step 1: Generate a self-signed certificate
|
||||
|
||||
Change `<your-domain>` in the example below, and create certificates with `openssl`:
|
||||
|
||||
```sh
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout volumes/api/server.key \
|
||||
-out volumes/api/server.crt \
|
||||
-subj "/CN=<your-domain>" && \
|
||||
chmod 640 volumes/api/server.key && \
|
||||
chgrp 65533 volumes/api/server.key
|
||||
```
|
||||
|
||||
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
|
||||
|
||||
### Step 2: Configure Kong for SSL
|
||||
|
||||
Comment out Kong's HTTP port mapping in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
kong:
|
||||
# ...
|
||||
ports:
|
||||
# - ${KONG_HTTP_PORT}:8000/tcp
|
||||
```
|
||||
|
||||
Uncomment the certificate volume mounts and SSL environment variables in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
kong:
|
||||
# ... existing configuration ...
|
||||
volumes:
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/server.crt:/home/kong/server.crt:ro
|
||||
- ./volumes/api/server.key:/home/kong/server.key:ro
|
||||
environment:
|
||||
# ... existing environment variables ...
|
||||
KONG_SSL_CERT: /home/kong/server.crt
|
||||
KONG_SSL_CERT_KEY: /home/kong/server.key
|
||||
```
|
||||
|
||||
### Step 3: Update configuration variables in `.env`
|
||||
|
||||
Edit your `.env` file to use HTTPS with the Kong HTTPS port:
|
||||
|
||||
```
|
||||
SUPABASE_PUBLIC_URL=https://<your-domain>:8443
|
||||
API_EXTERNAL_URL=https://<your-domain>:8443
|
||||
SITE_URL=https://<your-domain>:8443
|
||||
```
|
||||
|
||||
### Step 4: Restart and verify
|
||||
|
||||
```sh
|
||||
docker compose down && docker compose up -d
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -I -k https://<your-domain>:8443/auth/v1/
|
||||
```
|
||||
|
||||
The `-k` flag tells curl to accept the self-signed certificate.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Certificate not issued
|
||||
|
||||
If Caddy or Certbot fails to obtain a certificate:
|
||||
|
||||
- Verify that ports 80 and 443 are open on your firewall
|
||||
- Verify that your domain's DNS A record points to your server's public IP
|
||||
- Check proxy logs via `docker logs supabase-caddy` or `docker logs supabase-nginx`
|
||||
- Let's Encrypt has [rate limits](https://letsencrypt.org/docs/rate-limits/) - if you hit them, wait before retrying
|
||||
|
||||
### WebSocket connection failed
|
||||
|
||||
If Realtime subscriptions fail to connect:
|
||||
|
||||
- **Caddy** handles WebSocket upgrades automatically - check that Kong is healthy
|
||||
- **Nginx** requires explicit `Upgrade` and `Connection` headers on the `/realtime/v1/` location. Verify your `nginx.conf` includes these headers as shown above
|
||||
|
||||
### OAuth callback URL mismatch
|
||||
|
||||
If OAuth redirects fail with a callback URL error:
|
||||
|
||||
- Verify `API_EXTERNAL_URL` in `.env` is set to your HTTPS URL
|
||||
- Verify the callback URL registered with your OAuth provider matches `API_EXTERNAL_URL` followed by `/auth/v1/callback`
|
||||
- After changing `API_EXTERNAL_URL`, restart all services with `docker compose down && docker compose up -d`
|
||||
|
||||
### Mixed content warnings
|
||||
|
||||
If the browser console shows mixed content errors:
|
||||
|
||||
- Verify `SUPABASE_PUBLIC_URL` is set to your HTTPS URL
|
||||
- Verify `SITE_URL` is also set to HTTPS
|
||||
- Clear your browser cache after making changes
|
||||
|
||||
### ERR_CERT_AUTHORITY_INVALID
|
||||
|
||||
This is expected when using self-signed certificates. For production, use Caddy or Nginx with Let's Encrypt. If you need to use self-signed certificates, add the certificate to your system's trust store or use a browser flag to bypass the warning.
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Caddy documentation](https://caddyserver.com/docs/)
|
||||
- [Nginx documentation](https://nginx.org/en/docs/) (on nginx.org)
|
||||
- [docker-nginx-certbot on GitHub](https://github.com/JonasAlfredsson/docker-nginx-certbot)
|
||||
@@ -4,7 +4,7 @@ title: 'Log Drains'
|
||||
description: 'Getting started with Supabase Log Drains'
|
||||
---
|
||||
|
||||
Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
|
||||
Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
|
||||
|
||||
You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top).
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ database_id = "ba593989-12b5-464f-8ede-a525e1ca2ffb"
|
||||
|
||||
**What is Egress?**
|
||||
|
||||
Egress (also known as bandwidth) is any amount of network packets/bytes being streamed back to a connected client. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.
|
||||
Egress (also known as bandwidth) is any amount of network packets/bytes being streamed to a connected client from your project. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.
|
||||
|
||||
You can read about Unified egress, included quota, and how to check the egress usage here: https://supabase.com/docs/guides/platform/manage-your-usage/egress. Additionally, the [project reports](/dashboard/project/_/observability) have a few egress related stats. You can create a custom report to look into daily egress.
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ message = "connect ECONNREFUSED 1.2.3.4:5432"
|
||||
message = "psql: error: connection to server at \"db.xxxxxxxxxxxxxxxxxxxx.supabase.co\" (1.2.3.4), port 5432 failed: Connection refused Is the server running on that host and accepting TCP/IP connections?"
|
||||
---
|
||||
|
||||
If you're not able to connect to the Supabase database and see the error `connect ECONNREFUSED 1.2.3.4:5432` or `psql: error: connection to server at "db.xxxxxxxxxxxxxxxxxxxx.supabase.co" (1.2.3.4), port 5432 failed: Connection refused
|
||||
Is the server running on that host and accepting TCP/IP connections?`, this could be because there are banned IPs on your project caused by Fail2ban as it kicks in when attempting 2 wrong passwords in a row.
|
||||
If you're not able to connect to the Supabase database and see one of the errors below, this could be because there are banned IPs on your project caused by Fail2ban as it kicks in when attempting 2 wrong passwords in a row.
|
||||
|
||||
- `connect ECONNREFUSED 1.2.3.4:5432`
|
||||
- `psql: error: connection to server at "db.xxxxxxxxxxxxxxxxxxxx.supabase.co" (1.2.3.4), port 5432 failed: Connection refused
|
||||
Is the server running on that host and accepting TCP/IP connections?`
|
||||
- `Circuit breaker open: Unable to establish connection to upstream database`
|
||||
|
||||
These bans will clear after 30mins but you can unban the IPs using the Supabase CLI https://supabase.com/docs/guides/cli following the commands below.
|
||||
|
||||
|
||||
@@ -20,3 +20,45 @@ To overcome these limitations and implement robust folder management with hierar
|
||||
- **Implement RLS policies on `storage.objects`.** These policies must `JOIN` with your custom metadata table to enforce hierarchical access permissions based on your defined folder structure.
|
||||
- **Handle batch folder operations via your metadata table.** For operations like moving or renaming folders, update the relevant entries in your custom metadata table. Note that actual file paths in Storage are not directly altered by these operations.
|
||||
- **Optimize RLS policies for performance.** `JOIN`s in RLS policies can lead to performance degradation, especially with large datasets. Ensure proper indexing on your custom metadata table and consider using `SECURITY DEFINER` functions to optimize policy execution.
|
||||
|
||||
## Alternative approach: Using the S3 protocol for bulk operations
|
||||
|
||||
Supabase Storage also supports an S3-compatible API. This allows you to use tools like the AWS CLI to perform bulk file operations such as downloading, moving, or reorganizing objects more efficiently.
|
||||
|
||||
Install the AWS CLI by following the [AWS CLI installation guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
|
||||
|
||||
Create S3 credentials in Supabase using the [Supabase S3 authentication guide](/docs/guides/storage/s3/authentication?queryGroups=language&language=credentials).
|
||||
|
||||
Configure an AWS CLI profile using the credentials you generated in Supabase. The profile name can be anything, but it must match the value used in the following commands.
|
||||
|
||||
```shell
|
||||
aws configure --profile supabase-s3
|
||||
```
|
||||
|
||||
Download files from a bucket or prefix:
|
||||
|
||||
```shell
|
||||
aws s3 cp s3://bucket-name/folder-name ./download-target
|
||||
--profile supabase-s3
|
||||
--endpoint-url https://<project-ref>.supabase.co/storage/v1/s3
|
||||
--recursive
|
||||
--region <region>
|
||||
```
|
||||
|
||||
- Replace `bucket-name` with your bucket name.
|
||||
- Replace `folder-name` with the prefix you want to download, or omit it to download the entire bucket.
|
||||
- Replace `<project-ref>` with your Supabase project reference.
|
||||
- Replace `<region>` with your project's region (for example `eu-central-1`).
|
||||
- `./download-target` is the local directory where files will be saved.
|
||||
|
||||
Move or rename files using the `mv` command. Because folders in Supabase Storage are implemented as prefixes, renaming a folder is effectively moving objects from one prefix to another.
|
||||
|
||||
```shell
|
||||
aws s3 mv s3://bucket-name-one/folder-name-one s3://bucket-name-two/folder-name-two
|
||||
--profile supabase-s3
|
||||
--endpoint-url https://<project-ref>.supabase.co/storage/v1/s3
|
||||
--recursive
|
||||
--region <region>
|
||||
```
|
||||
|
||||
This method is useful for large-scale downloads, migrations, or reorganizing files within a bucket.
|
||||
@@ -5,7 +5,7 @@ import ReactMarkdown from 'react-markdown'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import Breadcrumbs from '~/components/Breadcrumbs'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import GuidesSidebar from '~/components/GuidesSidebar'
|
||||
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import type { WithRequired } from '~/features/helpers.types'
|
||||
@@ -71,7 +71,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
'relative',
|
||||
'transition-all ease-out',
|
||||
'duration-100',
|
||||
hideToc ? 'col-span-12' : 'col-span-12 md:col-span-9'
|
||||
'col-span-12 md:col-span-9'
|
||||
)}
|
||||
>
|
||||
<Breadcrumbs className="mb-2" />
|
||||
@@ -114,24 +114,23 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
{!hideToc && (
|
||||
<GuidesTableOfContents
|
||||
video={meta?.tocVideo}
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
'col-span-3 self-start',
|
||||
'sticky',
|
||||
/**
|
||||
* --header-height: height of nav
|
||||
* 1px: height of nav border
|
||||
* 2rem: content padding
|
||||
*/
|
||||
'top-[calc(var(--header-height)+1px+2rem)]',
|
||||
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
|
||||
'max-h-[calc(100vh-var(--header-height)-3rem)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<GuidesSidebar
|
||||
video={meta?.tocVideo}
|
||||
hideToc={hideToc}
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
'col-span-3 self-start',
|
||||
'sticky',
|
||||
/**
|
||||
* --header-height: height of nav
|
||||
* 1px: height of nav border
|
||||
* 2rem: content padding
|
||||
*/
|
||||
'top-[calc(var(--header-height)+1px+2rem)]',
|
||||
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
|
||||
'max-h-[calc(100vh-var(--header-height)-3rem)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TocAnchorsProvider>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import GuidesTableOfContents from '~/components/GuidesSidebar'
|
||||
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { type GuideFrontmatter } from '~/lib/docs'
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { globby } from 'globby'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')
|
||||
|
||||
/**
|
||||
* Reads <$Partial path="..." /> tags and replaces them with the file contents.
|
||||
* Recurses to handle nested partials.
|
||||
*/
|
||||
async function inlinePartials(content: string): Promise<string> {
|
||||
const partialRegex = /<\$Partial\s+path="([^"]+)"[^/]*\/>/g
|
||||
const matches = [...content.matchAll(partialRegex)]
|
||||
for (const [fullMatch, partialPath] of matches) {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(path.join(PARTIALS_DIR, partialPath), 'utf8')
|
||||
const { content: partialBody } = matter(raw)
|
||||
const inlined = await inlinePartials(partialBody)
|
||||
content = content.replace(fullMatch, inlined)
|
||||
} catch {
|
||||
content = content.replace(fullMatch, '')
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
/** Remove the minimum common leading whitespace from all non-empty lines. */
|
||||
function dedentBlock(text: string): string {
|
||||
const lines = text.split('\n')
|
||||
const nonEmpty = lines.filter((l) => /\S/.test(l))
|
||||
if (!nonEmpty.length) return text
|
||||
const minIndent = Math.min(...nonEmpty.map((l) => (l.match(/^([ \t]*)/) ?? ['', ''])[1].length))
|
||||
if (!minIndent) return text
|
||||
return lines.map((l) => l.slice(minIndent)).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts StepHikeCompact components to markdown ordered lists.
|
||||
* Each step becomes a numbered item: the title (from Details) is bolded on the item
|
||||
* line, and the full step body (Details + Code) is dedented and appended below.
|
||||
* Remaining JSX tags inside the body are later stripped by stripJsxTags.
|
||||
*/
|
||||
function convertStepHike(content: string): string {
|
||||
return content.replace(/<StepHikeCompact>([\s\S]*?)<\/StepHikeCompact>/g, (_, body) => {
|
||||
const items: string[] = []
|
||||
const stepRe = /<StepHikeCompact\.Step[^>]*>([\s\S]*?)<\/StepHikeCompact\.Step>/g
|
||||
let stepNum = 1
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = stepRe.exec(body)) !== null) {
|
||||
const stepBody = m[1]
|
||||
const titleMatch = stepBody.match(/<StepHikeCompact\.Details[^>]+title="([^"]*)"/)
|
||||
const title = titleMatch ? titleMatch[1] : ''
|
||||
// Dedent the entire step body so nested JSX indentation is removed.
|
||||
// Remaining component tags (Details, Code, Admonition…) are stripped later.
|
||||
const inner = dedentBlock(stepBody).trim()
|
||||
const item = title ? `${stepNum}. **${title}**\n\n${inner}` : `${stepNum}. ${inner}`
|
||||
items.push(item)
|
||||
stepNum++
|
||||
}
|
||||
return items.join('\n\n')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips JSX component tags (capitalized names, dot-notation, or $-prefixed)
|
||||
* while keeping their inner content. Also strips wrapper div and a elements.
|
||||
* Removes MDX JSX comment blocks. Strips unnecessary leading indentation from
|
||||
* non-code-block lines.
|
||||
*/
|
||||
function stripJsxTags(content: string): string {
|
||||
// Remove MDX/JSX comments {/* ... */}
|
||||
content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, '')
|
||||
|
||||
// Remove self-closing JSX components: <Component ... /> or <$Directive ... />
|
||||
content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*\/>/gs, '')
|
||||
|
||||
// Remove opening JSX component tags (possibly multi-line): <Component ...>
|
||||
content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*>/gs, '')
|
||||
|
||||
// Remove closing JSX component tags: </Component>
|
||||
content = content.replace(/<\/[\$A-Z][\w.]*>/g, '')
|
||||
|
||||
// Remove wrapper div and a elements used structurally in MDX (carry JSX props
|
||||
// like className which are not valid HTML; inner content such as img is preserved)
|
||||
content = content.replace(/<div(?:\s[^>]*)?\s*>/g, '')
|
||||
content = content.replace(/<\/div>/g, '')
|
||||
content = content.replace(/<a(?:\s[^>]*)?\s*>/g, '')
|
||||
content = content.replace(/<\/a>/g, '')
|
||||
|
||||
// Split on fenced code blocks to handle prose and code separately.
|
||||
// For prose (even segments): strip leading whitespace (removes JSX nesting indent).
|
||||
// For code blocks (odd segments): dedent the body to remove JSX nesting indent while
|
||||
// preserving relative code structure, then normalize the closing fence.
|
||||
const segments = content.split(/(```[\s\S]*?```)/g)
|
||||
content = segments
|
||||
.map((seg, i) => {
|
||||
if (i % 2 === 0) return seg.replace(/^[ \t]+/gm, '')
|
||||
return seg.replace(
|
||||
/^(```[^\n]*\n)([\s\S]*?)(\n[ \t]*```)$/,
|
||||
(_, open, body) => open + dedentBlock(body) + '\n```'
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
|
||||
// Collapse lines that are only whitespace to empty lines, then deduplicate blank lines
|
||||
content = content.replace(/^[^\S\n]+$/gm, '')
|
||||
content = content.replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
const files = await globby(['content/guides/**/!(_)*.mdx'])
|
||||
let warnings = 0
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const outPath = filePath
|
||||
.replace(/^content\/guides\//, 'public/docs/guides/')
|
||||
.replace(/\.mdx$/, '.md')
|
||||
|
||||
let output: string
|
||||
try {
|
||||
const raw = await fs.promises.readFile(filePath, 'utf8')
|
||||
const { content: rawContent, data } = matter(raw)
|
||||
|
||||
const withPartials = await inlinePartials(rawContent)
|
||||
const withSteps = convertStepHike(withPartials)
|
||||
const processed = stripJsxTags(withSteps)
|
||||
|
||||
const header = [
|
||||
data.title ? `# ${data.title}` : '',
|
||||
data.subtitle || data.description ? `\n${data.subtitle ?? data.description}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
output = header ? `${header}\n\n${processed}` : processed
|
||||
} catch (err) {
|
||||
warnings++
|
||||
console.warn(
|
||||
`[warn] Failed to process ${filePath}: ${err instanceof Error ? err.message : err}`
|
||||
)
|
||||
// Fall back to raw file content so the route still serves something
|
||||
try {
|
||||
output = await fs.promises.readFile(filePath, 'utf8')
|
||||
} catch {
|
||||
output = `<!-- failed to generate: ${filePath} -->`
|
||||
}
|
||||
}
|
||||
|
||||
// content/guides/ai/vector-columns.mdx → public/docs/guides/ai/vector-columns.md
|
||||
// Placing under public/docs/ ensures the file is served at /docs/guides/...
|
||||
// matching the exact URL of the rendered page.
|
||||
await fs.promises.mkdir(path.dirname(outPath), { recursive: true })
|
||||
await fs.promises.writeFile(outPath, output)
|
||||
})
|
||||
)
|
||||
|
||||
const summary = warnings ? ` (${warnings} with warnings)` : ''
|
||||
console.log(`Generated ${files.length} markdown files under public/docs/guides/${summary}`)
|
||||
}
|
||||
|
||||
generate()
|
||||
@@ -6,8 +6,19 @@ import { BASE_PATH } from '~/lib/constants'
|
||||
|
||||
const REFERENCE_PATH = `${BASE_PATH ?? ''}/reference`
|
||||
|
||||
const GUIDES_PATH = `${BASE_PATH ?? ''}/guides`
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Serve pre-generated .md files before the [[...slug]] page route can intercept them
|
||||
if (url.pathname.startsWith(GUIDES_PATH + '/') && url.pathname.endsWith('.md')) {
|
||||
const slug = url.pathname.slice(GUIDES_PATH.length + 1, -'.md'.length)
|
||||
const rewriteUrl = new URL(url)
|
||||
rewriteUrl.pathname = `${BASE_PATH ?? ''}/api/guides-md/${slug}`
|
||||
return NextResponse.rewrite(rewriteUrl)
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(REFERENCE_PATH)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
@@ -56,5 +67,5 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/reference/:path*',
|
||||
matcher: ['/reference/:path*', '/guides/:path*'],
|
||||
}
|
||||
@@ -60,6 +60,7 @@ const nextConfig = {
|
||||
],
|
||||
outputFileTracingIncludes: {
|
||||
'/api/crawlers': ['./features/docs/generated/**/*', './docs/ref/**/*'],
|
||||
'/api/guides-md/**/*': ['./public/docs/guides/**/*'],
|
||||
'/guides/**/*': ['./content/guides/**/*', './content/troubleshooting/**/*', './examples/**/*'],
|
||||
'/reference/**/*': ['./features/docs/generated/**/*', './docs/ref/**/*'],
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:llms": "tsx --conditions=react-server ./scripts/llms.ts",
|
||||
"build:guides-markdown": "tsx ./internals/generate-guides-markdown.ts",
|
||||
"build:sitemap": "tsx ./internals/generate-sitemap.ts",
|
||||
"clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__",
|
||||
"codegen:examples": "shx cp -r ../../examples ./examples",
|
||||
@@ -25,7 +26,7 @@
|
||||
"lint": "eslint .",
|
||||
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
|
||||
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm run build:guides-markdown",
|
||||
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"preembeddings": "pnpm run codegen:references",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -2,14 +2,17 @@ TEAM
|
||||
|
||||
Supabase is 100% remote.
|
||||
|
||||
Aaron Brooks
|
||||
Aaron Byrne
|
||||
Ada Wong
|
||||
Adam Mohammed
|
||||
Adam Mokan
|
||||
AJ Matias
|
||||
Akash Manimaran
|
||||
Alaister Young
|
||||
Alan De Los Santos
|
||||
Aleksi Immonen
|
||||
Alex Hall
|
||||
Alexander Korotkov
|
||||
Ali Waseem
|
||||
Allan S
|
||||
@@ -34,6 +37,7 @@ Brendan Stephens
|
||||
Brent Newson
|
||||
Carel de Waal
|
||||
Cameron Blackwood
|
||||
Cameron Michie
|
||||
Cemal Kılıç
|
||||
Chandana Anumula
|
||||
Charis Lam
|
||||
@@ -82,6 +86,7 @@ Francesco Sansalvadore
|
||||
Garrett Crowell
|
||||
Gerardo Estaba
|
||||
Gildas Garcia
|
||||
Giuseppe Mandato
|
||||
Greg Kress
|
||||
Greg P
|
||||
Greg Richardson
|
||||
@@ -108,7 +113,10 @@ Jess Shears
|
||||
Jim Brodeur
|
||||
Jim Chanco Jr
|
||||
Joakim Ahrlin
|
||||
Joaquim Moreno
|
||||
Joel Low
|
||||
Joel Martin
|
||||
Johan Bergström
|
||||
John Pena
|
||||
John Schaeffer
|
||||
Jon M
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92.2 65" style="enable-background:new 0 0 92.2 65;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#faf9f5;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="65" width="92.2" x="-43.7" y="-98">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path class="st0" d="M66.5,0H52.4l25.7,65h14.1L66.5,0z M25.7,0L0,65h14.4l5.3-13.6h26.9L51.8,65h14.4L40.5,0C40.5,0,25.7,0,25.7,0z
|
||||
M24.3,39.3l8.8-22.8l8.8,22.8H24.3z">
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 715 B |
@@ -1,16 +1,3 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92.2 65" style="enable-background:new 0 0 92.2 65;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#181818;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="65" width="92.2" x="-43.7" y="-98">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path class="st0" d="M66.5,0H52.4l25.7,65h14.1L66.5,0z M25.7,0L0,65h14.4l5.3-13.6h26.9L51.8,65h14.4L40.5,0C40.5,0,25.7,0,25.7,0z
|
||||
M24.3,39.3l8.8-22.8l8.8,22.8H24.3z">
|
||||
</path>
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.92381 15.2988L9.85798 13.0912L9.92381 12.8988L9.85798 12.7924H9.66558L9.00735 12.7519L6.75925 12.6912L4.80988 12.6102L2.92127 12.5089L2.44533 12.4076L1.99976 11.8203L2.04533 11.5266L2.44533 11.2583L3.01748 11.3089L4.2833 11.395L6.18203 11.5266L7.55925 11.6076L9.59976 11.8203H9.92381L9.96938 11.6886L9.85798 11.6076L9.77191 11.5266L7.80735 10.195L5.68077 8.78737L4.56684 7.97724L3.96431 7.56711L3.66052 7.1823L3.52887 6.3418L4.07571 5.73927L4.80988 5.7899L4.99722 5.84053L5.74153 6.41268L7.3314 7.64306L9.40735 9.17218L9.71115 9.42534L9.83267 9.33927L9.84786 9.27851L9.71115 9.05066L8.58203 7.01015L7.37697 4.9342L6.84026 4.07344L6.69849 3.55699C6.64786 3.34433 6.61241 3.16712 6.61241 2.94939L7.2352 2.10382L7.5795 1.99243L8.40988 2.10382L8.75925 2.40762L9.27571 3.58737L10.1111 5.4456L11.4074 7.97218L11.7871 8.72154L11.9896 9.41522L12.0656 9.62787H12.1972V9.50635L12.3036 8.08357L12.501 6.33673L12.6934 4.08863L12.7592 3.45572L13.0732 2.69623L13.696 2.2861L14.182 2.51901L14.582 3.09117L14.5263 3.46079L14.2884 5.00509L13.8225 7.42534L13.5187 9.0456H13.696L13.8985 8.84306L14.7187 7.75446L16.096 6.03294L16.7036 5.34939L17.4124 4.59496L17.8681 4.23547H18.7289L19.3618 5.17724L19.0782 6.14939L18.1922 7.27344L17.458 8.22534L16.4048 9.64306L15.7466 10.7772L15.8074 10.8684L15.9643 10.8532L18.3441 10.3469L19.6301 10.114L21.1643 9.85066L21.858 10.1747L21.9339 10.5038L21.6605 11.1772L20.02 11.5823L18.096 11.9671L15.2301 12.6456L15.1947 12.6709L15.2352 12.7215L16.5263 12.8431L17.0782 12.8734H18.4301L20.9466 13.0608L21.6048 13.4962L21.9998 14.0279L21.9339 14.4329L20.9213 14.9494L19.5542 14.6253L16.3643 13.8658L15.2706 13.5924H15.1187V13.6836L16.0301 14.5747L17.701 16.0836L19.7922 18.0279L19.8985 18.5089L19.6301 18.8886L19.3466 18.8481L17.5086 17.4658L16.7998 16.8431L15.1947 15.4912H15.0884V15.6329L15.458 16.1747L17.4124 19.1114L17.5137 20.0127L17.3719 20.3064L16.8656 20.4836L16.3086 20.3823L15.1643 18.7772L13.9846 16.9696L13.0327 15.3494L12.9162 15.4152L12.3542 21.4658L12.0909 21.7747L11.4833 22.0076L10.977 21.6228L10.7086 21L10.977 19.7696L11.301 18.1646L11.5643 16.8886L11.8023 15.3038L11.9441 14.7772L11.9339 14.7418L11.8175 14.757L10.6225 16.3975L8.80482 18.8532L7.36684 20.3924L7.02254 20.5291L6.42507 20.2203L6.48077 19.6684L6.81495 19.1772L8.80482 16.6456L10.0048 15.076L10.7795 14.1696L10.7744 14.038H10.7289L5.44279 17.4709L4.50102 17.5924L4.09596 17.2127L4.14659 16.5899L4.339 16.3874L5.92887 15.2937L5.92381 15.2988Z" fill="#D97757"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="36" height="36" fill="none"><path fill="url(#a)" d="M0 0h36v36H0z"/><defs><pattern id="a" width="1" height="1" patternContentUnits="objectBoundingBox"><use xlink:href="#b" transform="scale(.0005)"/></pattern><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAfQCAYAAACaOMR5AAAACXBIWXMAAAsTAAALEwEAmpwYABAAAElEQVR4nOz9edhl55mXhz5rD+VBLckqzYNlyZYnyZbVsixLtsqaSqoqqaHTEAgk0GmGQOgDDTTdTdNtS5ZMNz0EGprQDAnDARIydQYOkEOSQ0aSQBJCEgJR1TfUXJqH0lBV3x7W+aP2s+u3f9+zdsnG2pru+7rq2nuv4Z3W2uur77vf53mbtm0DAAAAAAAAAAAAAAAAAADg/U7v7W4AAAAAAAAAAAAAAAAAAADAOwEEOgAAAAAAAAAAAAAAAAAAQCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIgKBDgAAAAAAAAAAAAAAAAAAEBEIdAAAAAAAAAAAAAAAAAAAgIhAoAMAAAAAAAAAAAAAAAAAAEQEAh0AAAAAAAAAAAAAAAAAACAiEOgAAAAAAAAAAAAAAAAAAAARgUAHAAAAAAAAAAAAAAAAAACICAQ6AAAAAAAAAAAAAAAAAABARCDQAQAAAAAAAAAAAAAAAAAAIiJi8HY3AOD9RtM0b3cTAAAAAADgnccFl1566fUvvPDCP3u7GwIAAAAAAO882rZ9u5sA8L6BCHQAAAAAAACAt5Fbb71170//9E//N1/96lf/0VVXXfXLH/7wh695u9sEAAAAAAAA8H6lYcYKwGohAh0AAAAAACIirr/++pt37979+N133/0bR6NR/Oqv/mo888wzcckllxw/dOjQzx8+fPjPR8SZt7udAAAAAADw9oPPA1gdpHAHAAAAAAAAWCEXXXTRznvvvfcPPfTQQz/yoQ996HvOnDkTL774YnvmzJk4ceJEXHTRRdd8+tOf/lPXXHPND66vrz/23HPP/Z23u80AAAAAAAAA7xcQ6AAAAAAAAACrofnKV77yQ4888sjXr7766o+Px+M4depUu7W11bz22mvNaDSKM2fOxMmTJ9ter9dcdNFFX/ziF7/4t0+cOPGr6+vr33zttdf+ydvdAQAAAAAAAID3Ogh0AAAAAAAAgLeYz3zmM/c++uijT95yyy1fm0wmcebMmXY6ncbW1lbzxhtvxGuvvRZbW1vRtm288sorzY4dO6Lf77fD4bC59tprf/1VV121d2Nj488cOHDgFyPi+be7PwAAAAAAAADvVRDoAAAAAAAAAG8RH/3oRz9x7733fuOee+75rcPhsLe1tRXT6bQdjUbNZDKJ8Xgcp0+fjjNnzkTbtjGdTmM0GsWpU6diOBw2ERFt27b9fv+CT33qUz9xzTXX/Mb19fWfPXr06F+KiMnb3D0AAAAAAACA9xwIdAAAAAAAAIDvMpdddtmFX/rSl37kwQcf/NGdO3funKVnbyOiGY/HzWQyiclkEqdOnYpTp07FaDSKtm2j1+vNpfqOHTsiIuKDH/xg07ZtO51O48Mf/vANt95661+49tprf3Btbe3xF1544e+9vT0FAAAAAAAAeG+BQAcAAAAAAAD4LvLlL3/5N+3bt++xj370o5+NiNja2mqn02kTEc10Oo3pdBqzNO6R6563bRtN08RkMom2bePMmTNx+vTp6Pf70e/3IyKaXq8Xo9Go7fV6zaWXXnrPzp07/6tjx4799YMHD37r5MmTB97WTgMAAAAAAAC8R0CgAwAAAAAAAHwX+OQnP/nlvXv3PnHbbbftmU6nMR6PYxY53rRtO5fj4/E4ptNp5LYU6m3bRkTMP29tbcUHPvCBGI1G0TRNNE0TvV6vmaV6b4fDYXPdddf91iuuuOLXbGxs/MmNjY0/GRGvvK2DAAAAAAAAAPAuB4EOAAAAAAAA8M/Btddee92uXbt+6r777vudw+FwqOI8hXeub55yPAX5aDSK0WgU0+l0Xl4eu7W1FWfOnNEo9BgMBtHr9aLf7zej0SgGg0G7Y8eOj3z2s5/95rXXXvsvr62tPXnixIl/5+0aCwAAAAAAAIB3Owh0AAAAAAAAgO+MDz744IM/vHfv3h/fuXPnVdPpNEajURsRzWQyaSIitra25uuaTyaTGI/HERExHo9jNBrF1tbWQkS6yvbJZDIX7CnOJ5PJPNV7v9+P8Xjc9Hq9ttfrxYUXXvip22677a9fd911v219ff2xF1988X98OwcHAAAAAAAA4N0IAh0AAAAAAADg2+R7v/d7f+0jjzzyxMc//vHbmqaJra2ttmmamEWdLxybkedN08w/pzSfTCbz43K/4gI9IuaSXT43M7HeRkRz1VVXPXjZZZfde+jQob+0sbHxs6dPnz70Fg0DAAAAAAAAwHsOBDoAAAAAAADAm+TGG2/8wu7du5+46667vj/XM2/PLl7epNROKZ7rmue/lOanT5+OWbT6fFv+UzQSPaPRk+FwGJPJJHq9Xkyn06y36ff7MRqN2l6vN7jxxht/11VXXfUDm5ubv7i5uflnIuKNFQ4VAAAAAAAAwLsSBDoAAAAAAADAebjiiiuuvOeee378wQcf/OEPfehDH5pMJjGdTtu2Line truncated
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -1,100 +0,0 @@
|
||||
<svg width="1645" height="1645" viewBox="0 0 1645 1645" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_117_117)">
|
||||
<path d="M1 1368C1 1365.3 1.00001 1362.6 1.31425 1359.18C1.75627 1354.99 1.99572 1351.51 1.99594 1348.04C2.01862 996.25 2.02452 644.462 1.98645 292.674C1.98629 291.116 1.34296 289.558 1.00001 288C1 285.3 1 282.599 1.3018 279.255C2.72216 270.944 3.81982 263.274 4.9628 255.611C10.9567 215.425 24.7852 178.093 46.2174 143.622C56.587 126.943 67.9872 110.992 82.1009 97.4609C86.1754 108.569 90.2406 119.306 94.1741 130.091C102.501 152.922 110.72 175.793 119.141 198.589C119.656 199.982 121.541 200.87 122.712 202.329C105.52 234.057 97.074 267.504 97.0882 303.386C97.2254 650.036 97.1807 996.686 97.1015 1343.34C97.0988 1355.32 97.736 1367.19 99.925 1378.97C112.624 1447.28 150.162 1497.2 211.845 1528.78C240.51 1543.45 271.39 1549.5 303.525 1549.47C355.856 1549.43 408.187 1549.46 460.518 1549.48C462.671 1549.48 464.825 1549.71 466.987 1550.19C467.321 1552.17 467.449 1553.86 468.005 1555.41C473.992 1572.01 480.095 1588.57 486.065 1605.18C490.792 1618.33 495.377 1631.54 499.556 1644.76C496.756 1644.83 494.424 1644.88 492.092 1644.88C428.943 1644.88 365.794 1644.86 302.645 1644.92C300.43 1644.92 298.215 1645.62 296 1646C292.638 1646 289.275 1646 285.194 1645.67C268.173 1645.23 252.366 1641.59 236.602 1638.13C182.066 1626.16 134.448 1600.87 94.2383 1562.13C83.5056 1551.79 73.4257 1540.6 64.2199 1528.88C31.3503 1487.03 11.8664 1439.34 4.02644 1386.8C3.08977 1380.52 2.01224 1374.27 1 1368Z" fill="#1A80FD"/>
|
||||
<path d="M122.788 201.993C121.54 200.87 119.655 199.982 119.141 198.589C110.72 175.793 102.501 152.922 94.1738 130.091C90.2403 119.306 86.1752 108.57 82.2373 97.1523C89.604 89.4547 96.5958 82.0607 104.252 75.4348C150.343 35.5486 203.578 11.5302 264.161 3.97824C270.138 3.23316 276.055 2.00431 282 1C283.714 1 285.428 1 287.843 1.32795C290.187 1.81222 291.831 2.10447 293.475 2.10505C338.638 2.12105 383.801 2.09055 428.964 2.10406C462.96 2.11422 496.956 2.17447 531.045 2.54989C539.454 25.5147 547.835 48.1185 556.054 70.7812C559.219 79.5101 562.016 88.3727 564.531 97.2984C561.748 97.4599 559.416 97.5297 557.084 97.5298C471.255 97.5354 385.425 97.4877 299.596 97.6069C291.003 97.6188 282.346 98.1457 273.832 99.2778C240.886 103.658 210.792 115.506 183.62 134.699C158.307 152.579 138.261 175.218 122.788 201.993Z" fill="#0C8BFB"/>
|
||||
<path d="M1180 1646C1163.31 1646 1146.62 1646 1129.26 1645.81C1128.06 1645.4 1127.54 1645.19 1127.01 1644.59C1126.84 1643.22 1126.83 1642.18 1126.5 1641.27C1118.14 1618.34 1109.68 1595.45 1101.4 1572.5C1098.71 1565.03 1096.48 1557.39 1094.45 1549.7C1112.24 1549.56 1129.62 1549.54 1147 1549.89C1147.15 1551.41 1147.1 1552.63 1147.49 1553.69C1156.69 1578.94 1166 1604.14 1175.13 1629.42C1177.08 1634.83 1178.39 1640.47 1180 1646Z" fill="#1D8DF9"/>
|
||||
<path d="M1180.47 1646C1178.39 1640.47 1177.08 1634.83 1175.13 1629.42C1166 1604.14 1156.69 1578.94 1147.49 1553.69C1147.1 1552.63 1147.15 1551.41 1147.47 1549.88C1164.31 1549.6 1180.68 1549.72 1197.06 1550.23C1197.23 1551.44 1197.28 1552.29 1197.55 1553.06C1208.29 1582.73 1219 1612.41 1229.94 1642C1230.46 1643.39 1233.06 1644.83 1234.71 1644.84C1251.49 1645.03 1268.28 1644.92 1285.53 1644.91C1302.99 1644.94 1320 1644.95 1337.47 1644.97C1345.59 1645.02 1353.24 1645.06 1360.88 1645.09C1360.88 1645.09 1360.89 1645.55 1360.95 1645.78C1300.98 1646 1240.96 1646 1180.47 1646Z" fill="#2E8CF7"/>
|
||||
<path d="M871 1C882.354 1 893.708 1 905.856 1.22607C909.101 1.64977 911.554 1.8474 914.031 2.42746C922.016 25.1597 930.032 47.4902 937.913 69.8686C941.098 78.913 943.987 88.0617 946.554 97.2827C932.07 97.43 918.042 97.4576 904.013 97.109C903.858 95.5885 903.903 94.3713 903.518 93.3107C895.31 70.7174 886.966 48.1733 878.855 25.5455C875.958 17.4637 873.602 9.18749 871 1Z" fill="#9F87EC"/>
|
||||
<path d="M870.531 1C873.603 9.18749 875.958 17.4637 878.855 25.5455C886.966 48.1733 895.31 70.7174 903.518 93.3107C903.903 94.3713 903.858 95.5885 903.544 97.1267C896.992 97.5232 890.91 97.5259 884.778 97.4196C884.454 97.2001 884.267 97.2728 883.749 97.5269C877.222 97.4047 871.118 97.2844 864.946 96.7841C854.539 67.8342 844.17 39.2751 833.91 10.677C832.919 7.91346 832.602 4.90787 832.425 2.01049C836.252 1.67087 839.626 1.33544 843 1C852.021 1 861.042 1 870.531 1Z" fill="#8D88EE"/>
|
||||
<path d="M1 288.464C1.34296 289.558 1.98628 291.116 1.98645 292.674C2.02451 644.462 2.01861 996.25 1.99594 1348.04C1.99571 1351.51 1.75626 1354.99 1.31425 1358.73C0.999999 1002.31 0.999999 645.619 1 288.464Z" fill="#4F73D6"/>
|
||||
<path d="M1646 1354.55C1646 1355 1645.53 1355.03 1645.25 1354.59C1644.9 1326.76 1644.85 1299.37 1644.84 1271.52C1644.86 1235.36 1644.83 1199.67 1644.84 1163.52C1644.92 1104.85 1644.97 1046.66 1644.99 988.459C1644.99 984.638 1644.88 980.816 1644.87 976.529C1644.95 906.369 1644.98 836.675 1645 766.981C1645 765.987 1644.89 764.992 1644.88 763.531C1644.95 722.827 1645 682.591 1645 642.355C1645 623.57 1644.9 604.786 1644.9 585.535C1644.97 556.985 1645.02 528.901 1645 500.817C1644.99 481.211 1644.9 461.605 1644.9 441.534C1644.97 435.585 1644.99 430.102 1645 424.619C1645.03 380.424 1645.06 336.228 1645.08 292.033C1645.08 292.033 1645.54 292.011 1645.77 292.005C1646 646.031 1646 1000.06 1646 1354.55Z" fill="#4F73D6"/>
|
||||
<path d="M296.469 1646C298.215 1645.62 300.43 1644.92 302.645 1644.92C365.794 1644.86 428.943 1644.88 492.092 1644.88C494.424 1644.88 496.756 1644.83 500.022 1644.8C503.452 1644.84 505.948 1644.9 508.444 1644.9C579.524 1644.93 650.604 1644.97 721.684 1644.96C766.128 1644.96 810.572 1644.88 855.483 1644.87C877.307 1644.94 898.665 1644.98 920.032 1645.26C920.041 1645.5 920 1646 920 1646C712.313 1646 504.625 1646 296.469 1646Z" fill="#4F73D6"/>
|
||||
<path d="M530.952 2.21249C496.956 2.17453 462.96 2.11429 428.964 2.10412C383.801 2.09062 338.638 2.12111 293.475 2.10512C291.831 2.10454 290.187 1.81229 288.271 1.32802C424.354 1.00007 560.708 1.00007 697.508 1.25306C697.952 1.67538 697.949 1.8447 697.497 2.00216C693.99 2.01947 690.933 2.04864 687.416 2.0665C668.93 2.09266 650.906 2.13014 632.424 2.13943C615.622 2.14076 599.276 2.17029 582.467 2.17389C564.986 2.16948 547.969 2.19098 530.952 2.21249Z" fill="#4F73D6"/>
|
||||
<path d="M1364.53 1C1365 1 1365.02 1.46267 1364.55 1.75103C1352.72 2.05887 1341.36 2.07835 1329.54 2.02485C1299.38 1.99944 1269.68 2.047 1239.53 2.02715C1212.71 2.01637 1186.36 2.07301 1159.54 2.07056C1140.05 2.04866 1121.03 2.08586 1101.54 2.06945C1083.04 2.0179 1065.01 2.01996 1046.54 1.97653C1043.39 1.87285 1040.69 1.81467 1038 1.56736C1038 1.37824 1038 1 1038 1C1146.69 1 1255.38 1 1364.53 1Z" fill="#4F73D6"/>
|
||||
<path d="M920.469 1646C920 1646 920.041 1645.5 920.498 1645.25C989.644 1644.99 1058.33 1644.99 1127.02 1644.99C1127.54 1645.19 1128.06 1645.4 1128.79 1645.81C1059.65 1646 990.292 1646 920.469 1646Z" fill="#2E8CF7"/>
|
||||
<path d="M697.946 2.01395C697.949 1.84463 697.952 1.67531 697.977 1.25299C746.021 1 794.042 1 842.531 1C839.626 1.33544 836.252 1.67087 831.959 2.0073C816.345 2.0221 801.649 2.03592 786.49 2.04194C770.674 2.04536 755.322 2.05657 739.503 2.05892C725.34 2.03802 711.643 2.02599 697.946 2.01395Z" fill="#8089EF"/>
|
||||
<path d="M1037.53 1C1038 1 1038 1.37824 1037.53 1.63439C1018.04 1.94618 999.001 2.00183 979.499 2.03838C957.357 2.02786 935.682 2.03644 914.007 2.04503C911.554 1.8474 909.101 1.64977 906.324 1.22607C949.688 1 993.375 1 1037.53 1Z" fill="#8089EF"/>
|
||||
<path d="M865.014 97.1641C871.118 97.2844 877.222 97.4047 883.91 97.5941C884.494 97.6632 884.827 97.5286 884.827 97.5286C890.91 97.5259 896.992 97.5232 903.544 97.5029C918.042 97.4576 932.07 97.43 947.01 97.3979C963.278 97.4451 978.636 97.4966 994.094 97.6684C994.195 97.7885 994.656 97.8216 994.656 97.8216C994.656 97.8216 995.011 97.5482 995.477 97.5173C1001.29 97.3786 1006.64 97.2708 1012.45 97.2845C1020.2 97.4457 1027.48 97.5468 1034.77 97.5121C1049.5 97.4418 1064.23 97.2978 1079.41 97.3166C1098.24 97.3539 1116.62 97.2586 1135.43 97.2784C1154.91 97.322 1173.97 97.2505 1193.47 97.3086C1197.28 97.4669 1200.66 97.4957 1204.26 97.5849C1204.5 97.6452 1204.97 97.5242 1205.31 97.5288C1206.13 97.5341 1206.59 97.5346 1207.27 97.6517C1207.94 97.7002 1208.4 97.6321 1209.24 97.5366C1210.11 97.5304 1210.61 97.5517 1211.19 97.669C1211.26 97.7651 1211.62 97.7914 1211.62 97.7914C1211.62 97.7914 1211.89 97.5728 1212.36 97.5345C1232.87 97.386 1252.91 97.2758 1273.34 97.2964C1293.26 97.4487 1312.79 97.47 1332.71 97.6349C1335.35 97.6996 1337.6 97.6207 1340.31 97.5386C1342.53 97.5376 1344.31 97.5399 1346.18 97.6281C1346.27 97.7141 1346.7 97.5561 1347.18 97.5108C1349.93 97.5395 1352.19 97.6136 1354.45 97.7391C1354.44 97.7907 1354.56 97.6948 1355.03 97.7458C1358 97.8471 1360.51 97.8974 1363.41 98.074C1372.74 99.6468 1381.74 100.818 1390.62 102.598C1407.79 106.043 1423.82 112.808 1439.84 120.464C1443.96 122.978 1447.88 125.172 1451.71 127.501C1469.48 138.295 1484.95 151.842 1499.09 167.432C1531.59 205.716 1548.27 249.627 1549 299.554C1549.17 311.027 1549.26 322.502 1549.3 334.44C1549.2 393.776 1549.19 452.647 1549.18 511.518C1549.18 512.344 1549.32 513.17 1549.31 514.353C1549.28 515.434 1549.33 516.159 1549.3 517.353C1549.2 586.741 1549.19 655.659 1549.18 724.578C1549.18 725.404 1549.33 726.23 1549.33 727.511C1549.46 733.093 1549.66 738.22 1549.62 743.518C1549.37 744.08 1549.37 744.471 1549.29 745.333C1549.2 800.715 1549.18 855.626 1549.17 910.538C1549.17 911.364 1549.31 912.19 1549.31 913.477C1549.2 920.269 1549.15 926.601 1549.16 932.933C1549.23 962.255 1549.31 991.578 1549.31 1021.33C1549.21 1032.4 1549.15 1043.04 1549.17 1053.68C1549.22 1075.79 1549.31 1097.89 1549.3 1120.46C1549.2 1148.33 1549.15 1175.74 1549.17 1203.15C1549.18 1215.77 1549.32 1228.39 1549.31 1241.47C1549.28 1260.33 1549.33 1278.73 1549.32 1297.31C1549.26 1297.5 1549.39 1297.87 1549.31 1298.27C1549.29 1299.49 1549.34 1300.31 1549.32 1301.31C1549.26 1301.5 1549.39 1301.87 1549.31 1302.28C1549.29 1303.5 1549.34 1304.32 1549.3 1305.47C1549.27 1306.48 1549.33 1307.16 1549.32 1308.22C1549.29 1309.1 1549.34 1309.61 1549.32 1310.31C1549.26 1310.5 1549.39 1310.87 1549.32 1311.24C1549.29 1312.12 1549.34 1312.62 1549.32 1313.31C1549.26 1313.5 1549.39 1313.87 1549.31 1314.34C1548.87 1332.54 1548.5 1350.27 1548.03 1368.4C1546.75 1375.41 1546.19 1382.21 1544.27 1388.61C1539.82 1403.42 1534.71 1418.03 1529.59 1432.95C1520.64 1450.83 1509.79 1467.03 1496.68 1482.09C1484.53 1492.65 1472.69 1503.07 1460.53 1513.65C1448.81 1520.22 1437.41 1526.63 1425.65 1533.05C1414.18 1536.77 1403.09 1540.48 1391.6 1544.16C1377.12 1545.96 1363.05 1547.77 1348.52 1549.55C1333.05 1549.6 1318.05 1549.71 1302.59 1549.69C1285.41 1549.64 1268.69 1549.72 1251.52 1549.68C1233.07 1549.64 1215.06 1549.74 1197.05 1549.83C1180.68 1549.72 1164.31 1549.6 1147.47 1549.5C1129.62 1549.54 1112.24 1549.56 1093.99 1549.58C1092.29 1549.54 1091.46 1549.48 1090.63 1549.48C1002.57 1549.47 914.499 1549.47 826.433 1549.48C824.947 1549.48 823.46 1549.71 821.525 1549.7C818.746 1549.54 816.416 1549.46 814.086 1549.46C701.202 1549.46 588.318 1549.47 475.434 1549.49C472.615 1549.49 469.797 1549.71 466.979 1549.83C464.825 1549.71 462.671 1549.48 460.518 1549.48C408.187 1549.46 355.856 1549.43 303.525 1549.47C271.391 1549.5 240.51 1543.45 211.845 1528.78C150.163 1497.2 112.624 1447.28 99.9252 1378.97C97.7362 1367.19 97.099 1355.32 97.1017 1343.34C97.1809 996.686 97.2256 650.036 97.0884 303.386C97.0742 267.504 105.52 234.057 122.712 202.329C138.261 175.218 158.308 152.579 183.621 134.699C210.792 115.506 240.886 103.658 273.832 99.2778C282.347 98.1457 291.004 97.6188 299.596 97.6069C385.426 97.4877 471.255 97.5354 557.085 97.5298C559.417 97.5297 561.749 97.4599 564.95 97.4234C582.536 97.3389 599.254 97.2537 616.426 97.2939C633.267 97.3355 649.653 97.2515 666.493 97.2771C684.953 97.3226 702.958 97.2586 721.418 97.3203C738.914 97.3587 755.953 97.2715 773.446 97.3086C789.275 97.3483 804.652 97.2638 820.478 97.3028C835.623 97.339 850.318 97.2515 865.014 97.1641ZM1119.4 699.846C1110.58 695.646 1101.77 691.445 1092.36 686.848C1080.57 681.243 1068.78 675.637 1056.49 669.551C1051.57 667.159 1046.66 664.73 1041.71 662.381C1027.18 655.476 1012.63 648.603 997.639 641.202C982.438 634.094 967.238 626.986 951.73 619.258C939.534 613.486 927.338 607.714 914.704 601.394C902.881 595.816 891.058 590.238 879.157 584.795C879.157 584.795 879.297 584.864 879.04 584.365C878.689 584.264 878.337 584.164 877.502 583.567C865.769 578 854.035 572.433 8Line truncated
|
||||
<path d="M466.987 1550.19C469.797 1549.71 472.615 1549.49 475.434 1549.49C588.318 1549.47 701.202 1549.46 814.086 1549.47C816.416 1549.47 818.746 1549.54 821.544 1550.04C822.278 1551.64 822.434 1552.81 822.828 1553.89C831.354 1577.27 839.975 1600.62 848.412 1624.03C850.876 1630.87 852.829 1637.89 855.017 1644.83C810.572 1644.88 766.128 1644.96 721.684 1644.96C650.604 1644.97 579.524 1644.93 508.444 1644.9C505.948 1644.9 503.452 1644.84 500.49 1644.76C495.377 1631.54 490.792 1618.33 486.065 1605.18C480.095 1588.57 473.992 1572.01 468.005 1555.41C467.45 1553.86 467.322 1552.17 466.987 1550.19Z" fill="#1187FC"/>
|
||||
<path d="M615.972 97.1686C599.254 97.2538 582.536 97.3391 565.4 97.2994C562.016 88.3728 559.22 79.5102 556.054 70.7813C547.835 48.1187 539.454 25.5148 531.045 2.55001C547.969 2.19103 564.986 2.16953 582.493 2.5504C583.108 3.77014 583.099 4.63623 583.377 5.39779C591.723 28.2888 600.166 51.1449 608.429 74.0658C611.175 81.6831 613.47 89.463 615.972 97.1686Z" fill="#1D8DF9"/>
|
||||
<path d="M1549.39 333.975C1549.26 322.501 1549.17 311.027 1549 299.554C1548.27 249.627 1531.59 205.716 1499.1 166.99C1498.69 163.782 1498.85 161.157 1498.04 158.882C1491.08 139.447 1483.91 120.086 1476.86 100.683C1467.88 75.9587 1458.91 51.2292 1450.08 26.4509C1449.5 24.8224 1450.14 22.7606 1450.2 20.9004C1456.45 23.3556 1462.89 25.4171 1468.9 28.3534C1479.31 33.4402 1489.46 39.0689 1499.9 45.1531C1500.58 48.5891 1500.72 51.4749 1501.66 54.0744C1508.87 74.125 1516.26 94.1077 1523.54 114.135C1534.85 145.273 1546.06 176.448 1557.4 207.575C1573.69 252.309 1590.1 297.002 1606.39 341.736C1617.73 372.864 1628.91 404.049 1640.31 435.154C1641.23 437.653 1643.3 439.728 1644.84 441.999C1644.9 461.605 1644.99 481.211 1645 500.816C1645.02 528.901 1644.97 556.985 1644.59 585.405C1643 583.231 1641.57 580.802 1640.61 578.197C1634.86 562.578 1629.25 546.91 1623.55 531.273C1610.79 496.248 1597.99 461.236 1585.23 426.209C1574.47 396.654 1563.77 367.074 1552.92 337.553C1552.39 336.122 1550.59 335.157 1549.39 333.975Z" fill="#E8526A"/>
|
||||
<path d="M1644.9 441.534C1643.3 439.728 1641.23 437.653 1640.31 435.154C1628.91 404.049 1617.73 372.865 1606.39 341.736C1590.1 297.002 1573.69 252.309 1557.4 207.575C1546.06 176.448 1534.85 145.273 1523.54 114.135C1516.26 94.1078 1508.87 74.125 1501.66 54.0745C1500.72 51.4749 1500.58 48.5891 1500.13 45.4197C1526.1 60.1911 1548.78 79.3385 1568.72 101.712C1602.82 139.955 1626.03 183.987 1637.1 234.042C1640.55 249.686 1641.79 265.824 1644 281.741C1644.43 284.866 1644.69 288.016 1645.05 291.594C1645.05 336.228 1645.03 380.423 1645 424.619C1644.99 430.102 1644.97 435.585 1644.9 441.534Z" fill="#ED4E5F"/>
|
||||
<path d="M1439.6 120.206C1423.82 112.808 1407.79 106.043 1390.62 102.598C1381.74 100.818 1372.74 99.6468 1363.38 97.6902C1362.67 96.0541 1362.47 94.8946 1362.08 93.8071C1353.03 68.7784 1343.89 43.7784 1334.92 18.7185C1332.98 13.2862 1331.63 7.64357 1330 2.09784C1341.36 2.07836 1352.72 2.05888 1364.55 1.98267C1370.57 2.58856 1376.13 3.16529 1381.66 3.93661C1387.9 4.80722 1394.12 5.85283 1400.73 7.3289C1405.39 19.8778 1409.64 31.9385 1413.99 43.9587C1422.54 67.5508 1431.19 91.107 1439.66 114.728C1440.24 116.34 1439.65 118.371 1439.6 120.206Z" fill="#DE5B80"/>
|
||||
<path d="M1439.84 120.464C1439.65 118.371 1440.24 116.34 1439.66 114.728C1431.19 91.1071 1422.54 67.5509 1413.99 43.9588C1409.64 31.9386 1405.39 19.8779 1401.11 7.46191C1417.31 11.5162 1433.5 15.9442 1449.95 20.6364C1450.13 22.7608 1449.5 24.8227 1450.08 26.4511C1458.91 51.2295 1467.88 75.9589 1476.86 100.683C1483.91 120.086 1491.08 139.448 1498.04 158.882C1498.85 161.157 1498.69 163.782 1498.97 166.686C1484.95 151.842 1469.48 138.295 1451.71 127.502C1447.88 125.172 1443.96 122.978 1439.84 120.464Z" fill="#E45672"/>
|
||||
<path d="M1549.4 1241.01C1549.32 1228.39 1549.18 1215.77 1549.17 1203.15C1549.15 1175.74 1549.2 1148.33 1549.63 1120.5C1551.31 1121.72 1553.07 1123.16 1553.75 1125C1568.31 1164.7 1582.81 1204.41 1597.19 1244.17C1611.23 1282.99 1625.08 1321.87 1639.15 1360.68C1640.12 1363.33 1642.25 1365.57 1643.83 1368C1642.42 1379.26 1641.76 1390.68 1639.4 1401.73C1636.24 1416.53 1631.75 1431.05 1627.38 1445.7C1624.78 1440.5 1622.45 1435.37 1620.52 1430.09C1607.65 1394.97 1594.85 1359.83 1582.07 1324.69C1572.35 1297.98 1562.72 1271.24 1552.92 1244.56C1552.39 1243.14 1550.6 1242.18 1549.4 1241.01Z" fill="#C274BD"/>
|
||||
<path d="M1643.89 1367.55C1642.25 1365.57 1640.12 1363.33 1639.15 1360.68C1625.08 1321.87 1611.23 1282.99 1597.19 1244.17C1582.81 1204.41 1568.31 1164.7 1553.75 1125C1553.07 1123.16 1551.31 1121.72 1549.71 1120.04C1549.31 1097.89 1549.22 1075.79 1549.17 1053.68C1549.15 1043.04 1549.21 1032.41 1549.69 1021.43C1551.19 1022.49 1552.63 1023.74 1553.21 1025.31C1562.82 1051.21 1572.38 1077.12 1581.84 1103.07C1601.85 1157.95 1621.78 1212.85 1641.81 1267.71C1642.39 1269.29 1643.78 1270.57 1644.79 1271.99C1644.85 1299.37 1644.9 1326.76 1645.01 1354.62C1644.69 1359.1 1644.32 1363.1 1643.89 1367.55Z" fill="#C670B3"/>
|
||||
<path d="M1549.31 1241.47C1550.6 1242.18 1552.39 1243.14 1552.92 1244.56C1562.72 1271.24 1572.35 1297.98 1582.07 1324.69C1594.85 1359.83 1607.65 1394.97 1620.52 1430.09C1622.45 1435.37 1624.78 1440.5 1627.17 1446.01C1619.97 1462.72 1612.54 1479.14 1605.04 1495.54C1604.22 1497.33 1602.99 1498.94 1601.33 1500.8C1599.43 1499.34 1597.65 1497.93 1596.96 1496.1C1591.83 1482.57 1586.92 1468.95 1581.95 1455.35C1571.73 1427.38 1561.54 1399.4 1551.22 1371.46C1550.73 1370.11 1549.18 1369.15 1548.13 1368C1548.5 1350.27 1548.87 1332.54 1549.38 1314.26C1549.62 1313.48 1549.57 1313.29 1549.39 1313.12C1549.34 1312.62 1549.29 1312.12 1549.39 1311.16C1549.62 1310.48 1549.57 1310.29 1549.39 1310.12C1549.34 1309.61 1549.29 1309.1 1549.45 1308.01C1549.56 1306.67 1549.48 1305.91 1549.39 1305.14C1549.34 1304.32 1549.29 1303.5 1549.39 1302.19C1549.62 1301.48 1549.57 1301.29 1549.39 1301.12C1549.34 1300.31 1549.29 1299.49 1549.39 1298.19C1549.62 1297.48 1549.57 1297.29 1549.39 1297.12C1549.33 1278.73 1549.28 1260.33 1549.31 1241.47Z" fill="#BC79C9"/>
|
||||
<path d="M1496.98 1481.95C1509.79 1467.03 1520.64 1450.83 1529.81 1433.24C1531.25 1434.84 1532.48 1436.27 1533.09 1437.93C1544.4 1469 1555.6 1500.11 1566.97 1531.16C1568.25 1534.66 1570.46 1537.82 1572.24 1541.14C1561.44 1551.88 1550.66 1562.64 1539.8 1573.34C1538.22 1574.9 1536.29 1576.11 1533.9 1577.61C1531.97 1575.31 1530.31 1573.01 1529.37 1570.46C1518.51 1540.98 1507.76 1511.46 1496.98 1481.95Z" fill="#B282DF"/>
|
||||
<path d="M1572.53 1540.97C1570.46 1537.82 1568.25 1534.66 1566.97 1531.16C1555.6 1500.11 1544.4 1469 1533.09 1437.93C1532.48 1436.27 1531.25 1434.84 1530.09 1433.01C1534.71 1418.03 1539.82 1403.42 1544.27 1388.61C1546.19 1382.21 1546.75 1375.41 1548.03 1368.4C1549.18 1369.15 1550.73 1370.11 1551.22 1371.46C1561.54 1399.4 1571.73 1427.38 1581.95 1455.35C1586.92 1468.95 1591.83 1482.57 1596.96 1496.1C1597.65 1497.93 1599.43 1499.34 1601.1 1501.01C1591.92 1514.32 1582.37 1527.57 1572.53 1540.97Z" fill="#B77ED4"/>
|
||||
<path d="M1496.68 1482.09C1507.76 1511.46 1518.51 1540.98 1529.37 1570.46C1530.31 1573.01 1531.97 1575.31 1533.62 1577.79C1529.44 1581.65 1525.21 1585.83 1520.37 1589.13C1511.75 1595.03 1502.76 1600.39 1493.5 1605.67C1486.35 1586.49 1479.56 1567.63 1472.9 1548.73C1468.78 1537.02 1464.86 1525.24 1460.85 1513.49C1472.69 1503.07 1484.53 1492.65 1496.68 1482.09Z" fill="#AC86E8"/>
|
||||
<path d="M1303.05 1549.81C1318.05 1549.71 1333.05 1549.6 1348.53 1549.92C1349.41 1552.27 1349.59 1554.3 1350.25 1556.14C1360.39 1584.09 1370.57 1612.03 1380.86 1639.93C1381.32 1641.17 1382.95 1641.99 1384.04 1643C1376.64 1643.67 1369.24 1644.35 1361.36 1645.06C1353.24 1645.06 1345.59 1645.02 1337.4 1644.61C1330.52 1626.71 1324.15 1609.2 1317.86 1591.66C1312.87 1577.73 1307.98 1563.77 1303.05 1549.81Z" fill="#698AF1"/>
|
||||
<path d="M1384.47 1642.99C1382.95 1641.99 1381.32 1641.17 1380.86 1639.93C1370.57 1612.03 1360.39 1584.09 1350.26 1556.14C1349.59 1554.3 1349.41 1552.27 1348.99 1549.96C1363.05 1547.77 1377.12 1545.96 1391.44 1544.48C1391.67 1546.27 1391.23 1547.86 1391.69 1549.13C1401.1 1575.19 1410.59 1601.22 1420.15 1627.23C1421.14 1629.92 1422.66 1632.4 1423.94 1634.98C1410.93 1637.65 1397.92 1640.31 1384.47 1642.99Z" fill="#7B89EF"/>
|
||||
<path d="M1460.53 1513.65C1464.86 1525.24 1468.78 1537.02 1472.9 1548.73C1479.56 1567.63 1486.35 1586.49 1493.17 1605.76C1482.99 1611.35 1472.72 1616.56 1461.74 1621.85C1459.68 1620.75 1457.62 1619.8 1457.06 1618.3C1446.61 1589.92 1436.33 1561.47 1426.02 1533.04C1437.41 1526.63 1448.81 1520.22 1460.53 1513.65Z" fill="#9F87EC"/>
|
||||
<path d="M1425.65 1533.05C1436.33 1561.47 1446.61 1589.92 1457.06 1618.3C1457.62 1619.8 1459.68 1620.74 1461.39 1621.99C1450.72 1625.97 1439.69 1629.91 1427.93 1633.91C1426.14 1634.29 1425.08 1634.62 1424.03 1634.95C1424.03 1634.95 1424 1635.01 1423.97 1635C1422.66 1632.4 1421.14 1629.92 1420.15 1627.23C1410.59 1601.22 1401.1 1575.19 1391.69 1549.13C1391.23 1547.86 1391.67 1546.27 1391.84 1544.51C1403.09 1540.48 1414.18 1536.77 1425.65 1533.05Z" fill="#8D88EE"/>
|
||||
<path d="M1424.37 1634.94C1425.08 1634.62 1426.14 1634.29 1427.53 1633.99C1426.82 1634.32 1425.77 1634.63 1424.37 1634.94Z" fill="#7B89EF"/>
|
||||
<path d="M855.482 1644.87C852.829 1637.89 850.876 1630.87 848.412 1624.03C839.975 1600.62 831.354 1577.27 822.828 1553.89C822.434 1552.81 822.278 1551.64 821.993 1550.17C823.46 1549.71 824.946 1549.48 826.432 1549.48C914.499 1549.47 1002.57 1549.47 1090.63 1549.48C1091.46 1549.48 1092.29 1549.54 1093.58 1549.7C1096.48 1557.39 1098.71 1565.03 1101.4 1572.5C1109.68 1595.45 1118.14 1618.34 1126.5 1641.27C1126.83 1642.18 1126.84 1643.22 1127.01 1644.59C1058.33 1644.99 989.643 1644.99 920.489 1645C898.665 1644.97 877.307 1644.94 855.482 1644.87Z" fill="#0A8CFB"/>
|
||||
<path d="M1197.06 1550.23C1215.06 1549.74 1233.07 1549.64 1251.54 1550.02C1260.31 1573.61 1268.69 1596.7 1276.9 1619.86C1279.83 1628.13 1282.35 1636.54 1285.06 1644.89C1268.28 1644.92 1251.49 1645.03 1234.71 1644.84C1233.06 1644.83 1230.46 1643.39 1229.95 1642C1219 1612.41 1208.29 1582.73 1197.55 1553.06C1197.28 1552.29 1197.23 1551.44 1197.06 1550.23Z" fill="#448BF5"/>
|
||||
<path d="M1285.53 1644.91C1282.35 1636.54 1279.83 1628.13 1276.9 1619.86C1268.69 1596.7 1260.31 1573.61 1251.98 1550.15C1268.69 1549.72 1285.41 1549.64 1302.59 1549.69C1307.98 1563.76 1312.87 1577.73 1317.86 1591.66C1324.15 1609.2 1330.52 1626.71 1336.93 1644.59C1320 1644.95 1302.99 1644.94 1285.53 1644.91Z" fill="#5A8AF3"/>
|
||||
<path d="M993.993 97.5482C978.635 97.4966 963.278 97.445 947.465 97.2781C943.986 88.0616 941.098 78.9129 937.913 69.8685C930.032 47.4902 922.015 25.1596 914.03 2.4274C935.682 2.03638 957.357 2.0278 979.51 2.45257C980.424 5.1445 980.571 7.50763 981.343 9.6458C990.996 36.4115 1000.76 63.1359 1010.42 89.9014C1011.25 92.2112 1011.48 94.7373 1012 97.1629C1006.64 97.2707 1001.29 97.3785 995.222 97.4292C994.333 97.4307 994.163 97.4895 993.993 97.5482Z" fill="#AC86E8"/>
|
||||
<path d="M864.946 96.7841C850.318 97.2515 835.623 97.339 820.459 96.9587C819.777 95.5211 819.669 94.5124 819.333 93.587C810.874 70.3229 802.324 47.0914 793.951 23.7966C791.377 16.6345 789.273 9.30341 786.953 2.04974C801.649 2.03592 816.345 2.0221 831.506 2.01147C832.602 4.90787 832.919 7.91346 833.91 10.677C844.17 39.2751 854.539 67.8342 864.946 96.7841Z" fill="#7B89EF"/>
|
||||
<path d="M884.778 97.4197C884.827 97.5287 884.494 97.6633 884.332 97.596C884.267 97.2729 884.453 97.2002 884.778 97.4197Z" fill="#175395"/>
|
||||
<path d="M1549.4 727.056C1549.33 726.23 1549.18 725.404 1549.18 724.578C1549.19 655.659 1549.2 586.741 1549.44 517.097C1549.78 515.611 1549.9 514.849 1550.34 514.175C1551.84 516.204 1553.32 518.026 1554.13 520.107C1556.35 525.849 1558.25 531.715 1560.36 537.503C1577.06 583.331 1593.77 629.154 1610.49 674.975C1620.65 702.812 1630.79 730.654 1641.06 758.449C1641.81 760.485 1643.55 762.156 1644.83 763.997C1644.89 764.992 1645 765.987 1645 766.981C1644.98 836.675 1644.94 906.369 1644.54 976.439C1642.9 974.77 1641.26 972.866 1640.45 970.657C1630.71 944.125 1621.11 917.542 1611.44 890.984C1598.69 855.99 1585.92 821.007 1573.17 786.02C1566.67 768.214 1560.25 750.383 1553.6 732.634C1552.82 730.544 1550.83 728.904 1549.4 727.056Z" fill="#D9608B"/>
|
||||
<path d="M1549.33 727.511C1550.83 728.904 1552.82 730.544 1553.6 732.634C1560.25 750.383 1566.67 768.214 1573.17 786.02C1585.92 821.007 1598.69 855.99 1611.44 890.984C1621.11 917.542 1630.71 944.125 1640.45 970.657C1641.26 972.866 1642.9 974.77 1644.49 976.905C1644.88 980.816 1644.99 984.637 1644.99 988.459C1644.97 1046.66 1644.92 1104.85 1644.53 1163.39C1643.18 1161.87 1641.96 1160.09 1641.24 1158.13C1633.93 1138.11 1626.75 1118.05 1619.45 1098.02C1605.71 1060.33 1591.9 1022.65 1578.15 984.956C1569.99 962.589 1561.95 940.179 1553.67 917.857C1552.97 915.974 1550.85 914.618 1549.39 913.016C1549.31 912.19 1549.17 911.364 1549.17 910.538C1549.18 855.626 1549.2 800.715 1549.41 745.095C1549.7 744.04 1549.78 743.693 1549.87 743.346C1549.66 738.22 1549.46 733.093 1549.33 727.511Z" fill="#D0689F"/>
|
||||
<path d="M1644.88 763.53C1643.55 762.156 1641.81 760.485 1641.06 758.449C1630.79 730.654 1620.65 702.812 1610.49 674.975C1593.77 629.155 1577.06 583.331 1560.36 537.503C1558.25 531.715 1556.35 525.849 1554.13 520.107C1553.32 518.026 1551.84 516.204 1550.19 514.147C1549.7 514.031 1549.39 513.997 1549.39 513.997C1549.32 513.17 1549.18 512.344 1549.18 511.518C1549.19 452.647 1549.2 393.776 1549.3 334.44C1550.59 335.157 1552.39 336.122 1552.92 337.553C1563.77 367.074 1574.47 396.654 1585.23 426.209C1597.99 461.236 1610.79 496.248 1623.55 531.273C1629.25 546.91 1634.86 562.578 1640.61 578.197C1641.57 580.802 1643 583.231 1644.53 585.871C1644.9 604.786 1645 623.57 1645 642.355C1645 682.591 1644.95 722.827 1644.88 763.53Z" fill="#E15879"/>
|
||||
<path d="M1549.31 913.477C1550.85 914.617 1552.97 915.973 1553.67 917.857C1561.95 940.179 1569.99 962.589 1578.15 984.956C1591.9 1022.65 1605.71 1060.33 1619.45 1098.02C1626.75 1118.05 1633.93 1138.11 1641.24 1158.13C1641.96 1160.09 1643.18 1161.87 1644.48 1163.86C1644.82 1199.67 1644.86 1235.36 1644.84 1271.52C1643.78 1270.57 1642.39 1269.29 1641.81 1267.71C1621.78 1212.85 1601.85 1157.95 1581.84 1103.07C1572.38 1077.12 1562.82 1051.21 1553.21 1025.31C1552.63 1023.74 1551.19 1022.49 1549.77 1021C1549.31 991.577 1549.23 962.255 1549.16 932.933C1549.15 926.601 1549.2 920.269 1549.31 913.477Z" fill="#CA6DAB"/>
|
||||
<path d="M720.962 97.1948C702.957 97.2588 684.953 97.3227 666.464 96.8784C657.977 73.8943 649.938 51.4311 641.989 28.9359C638.85 20.0498 635.911 11.0925 632.881 2.16766C650.906 2.13018 668.93 2.0927 687.477 2.44068C696.354 25.9273 704.795 48.997 713.013 72.1463C715.942 80.3966 718.328 88.8397 720.962 97.1948Z" fill="#448BF5"/>
|
||||
<path d="M721.418 97.3204C718.328 88.8397 715.942 80.3966 713.013 72.1463C704.796 48.997 696.354 25.9273 687.938 2.452C690.934 2.04868 693.991 2.01951 697.497 2.0022C711.643 2.0261 725.34 2.03813 739.519 2.44725C740.11 3.6653 740.075 4.5395 740.352 5.29947C748.265 26.9816 756.291 48.6224 764.108 70.339C767.299 79.205 770.044 88.2312 772.993 97.1845C755.953 97.2716 738.914 97.3588 721.418 97.3204Z" fill="#5A8AF3"/>
|
||||
<path d="M632.425 2.1394C635.911 11.0925 638.85 20.0497 641.99 28.9359C649.938 51.431 657.977 73.8942 666.009 96.7689C649.653 97.2516 633.267 97.3355 616.426 97.294C613.471 89.463 611.175 81.6831 608.429 74.0657C600.166 51.1448 591.723 28.2888 583.377 5.39771C583.099 4.63615 583.108 3.77007 582.957 2.57624C599.276 2.17026 615.622 2.14074 632.425 2.1394Z" fill="#2E8CF7"/>
|
||||
<path d="M1329.54 2.02491C1331.63 7.64361 1332.98 13.2862 1334.92 18.7185C1343.89 43.7785 1353.03 68.7784 1362.08 93.8072C1362.47 94.8946 1362.67 96.0542 1362.98 97.564C1360.51 97.8975 1358 97.8471 1355.04 97.6836C1354.58 97.5704 1354.45 97.6877 1354.45 97.6877C1352.19 97.6136 1349.93 97.5396 1347.1 97.4163C1346.34 97.3114 1346.19 97.3698 1346.09 97.5422C1344.31 97.5399 1342.53 97.5377 1339.99 97.4156C1336.93 97.361 1334.63 97.4262 1332.33 97.4914C1312.79 97.4701 1293.26 97.4487 1273.33 96.8979C1265.25 74.8242 1257.5 53.3014 1249.91 31.7261C1246.45 21.9024 1243.29 11.975 1239.99 2.09461C1269.68 2.04705 1299.38 1.99949 1329.54 2.02491Z" fill="#D46496"/>
|
||||
<path d="M1239.53 2.02721C1243.29 11.975 1246.45 21.9024 1249.91 31.7261C1257.5 53.3014 1265.25 74.8242 1272.94 96.767C1252.91 97.2758 1232.87 97.3861 1212.17 97.4624C1211.5 97.4286 1211.11 97.573 1211.11 97.573C1210.61 97.5517 1210.11 97.5305 1209.03 97.4087C1207.98 97.3838 1207.52 97.4595 1207.06 97.5353C1206.59 97.5347 1206.13 97.5342 1205.22 97.4284C1204.47 97.2224 1204.23 97.2896 1204.03 97.5246C1200.65 97.4958 1197.28 97.467 1193.41 96.9784C1183.95 71.6887 1174.91 46.8841 1166.06 22.0119C1163.74 15.4924 1162.01 8.76282 1160.01 2.12972C1186.36 2.07307 1212.71 2.01644 1239.53 2.02721Z" fill="#CA6DAB"/>
|
||||
<path d="M1012.45 97.2845C1011.48 94.7374 1011.25 92.2113 1010.42 89.9015C1000.76 63.136 990.996 36.4116 981.343 9.64589C980.571 7.50772 980.424 5.14459 979.976 2.47176C999 2.00186 1018.04 1.94621 1037.53 1.82354C1040.69 1.8147 1043.39 1.87288 1046.54 2.39713C1047.41 5.30658 1047.46 7.88018 1048.29 10.1732C1056.79 33.6535 1065.48 57.0662 1073.96 80.5526C1075.93 85.9893 1077.31 91.6347 1078.96 97.1842C1064.23 97.2978 1049.5 97.4418 1034.77 97.5121C1027.48 97.5469 1020.2 97.4457 1012.45 97.2845Z" fill="#B57FD8"/>
|
||||
<path d="M1159.54 2.07049C1162.01 8.76268 1163.74 15.4922 1166.06 22.0118C1174.91 46.884 1183.95 71.6886 1192.98 96.8487C1173.97 97.2504 1154.91 97.3219 1135.41 96.8824C1134.59 94.93 1134.33 93.4452 1133.83 92.0538C1125.29 68.6943 1116.66 45.3718 1108.21 21.9797C1105.86 15.4626 1104.06 8.74734 1102 2.12299C1121.03 2.08579 1140.05 2.04859 1159.54 2.07049Z" fill="#C274BD"/>
|
||||
<path d="M1101.54 2.06953C1104.06 8.74748 1105.86 15.4627 1108.21 21.9798C1116.66 45.3719 1125.29 68.6945 1133.83 92.0539C1134.34 93.4454 1134.59 94.9301 1134.98 96.7674C1116.62 97.2587 1098.24 97.3539 1079.41 97.3167C1077.31 91.6348 1075.93 85.9894 1073.96 80.5526C1065.48 57.0662 1056.79 33.6536 1048.29 10.1732C1047.46 7.88022 1047.41 5.30663 1046.99 2.44266C1065.01 2.02003 1083.04 2.01797 1101.54 2.06953Z" fill="#BC79C9"/>
|
||||
<path d="M773.446 97.3087C770.044 88.2311 767.299 79.2049 764.108 70.339C756.292 48.6223 748.265 26.9815 740.352 5.29941C740.075 4.53944 740.111 3.66525 739.985 2.45606C755.323 2.05663 770.675 2.04542 786.49 2.04199C789.273 9.30346 791.377 16.6345 793.951 23.7966C802.325 47.0915 810.874 70.3229 819.333 93.587C819.67 94.5124 819.777 95.5212 820.009 96.8352C804.652 97.2638 789.275 97.3484 773.446 97.3087Z" fill="#698AF1"/>
|
||||
<path d="M787.842 729.083C774.813 722.94 761.742 716.884 748.761 710.641C665.589 670.637 582.444 630.576 499.247 590.625C496.221 589.172 494.733 587.738 494.741 584.052C494.874 524.259 494.832 464.465 494.844 404.671C494.844 403.242 495.005 401.814 495.131 399.712C522.551 412.936 549.471 425.919 576.717 439.449C577.825 441.62 578.37 443.418 579.422 444.841C596.319 467.709 613.262 490.542 630.232 513.356C668.539 564.853 706.863 616.338 745.189 667.821C758.89 686.227 772.664 704.581 786.248 723.073C787.39 724.628 787.339 727.059 787.842 729.083Z" fill="#0A8CFB"/>
|
||||
<path d="M739.017 981.293C763.969 969.211 788.903 957.092 813.877 945.057C850.654 927.333 887.459 909.67 924.769 892.283C925.693 893.309 926.025 894.089 926.518 894.75C954.214 931.89 981.974 968.983 1009.59 1006.18C1015.71 1014.41 1021.33 1023.01 1027.18 1031.44C998.001 1045.53 968.839 1059.65 939.641 1073.69C907.441 1089.18 875.208 1104.59 842.334 1119.9C841.116 1119.17 840.487 1118.62 839.998 1117.96C813.021 1081.69 786.029 1045.44 759.107 1009.13C752.293 999.937 745.707 990.576 739.017 981.293Z" fill="#0B8BFB"/>
|
||||
<path d="M738.682 981.37C745.707 990.576 752.292 999.937 759.107 1009.13C786.028 1045.44 813.021 1081.69 839.998 1117.96C840.487 1118.62 841.116 1119.17 841.984 1119.96C824.541 1128.78 806.815 1137.45 789.041 1146.03C762.279 1158.95 735.494 1171.82 708.696 1184.66C698.063 1189.76 687.368 1194.73 676.279 1199.47C675.255 1198 674.805 1196.71 674.028 1195.67C658.494 1174.71 642.913 1153.78 627.359 1132.83C609.528 1108.82 591.715 1084.79 573.894 1060.77C589.623 1053.13 605.332 1045.44 621.086 1037.86C660.162 1019.03 699.259 1000.25 738.682 981.37Z" fill="#1583FC"/>
|
||||
<path d="M573.537 1060.84C591.715 1084.79 609.528 1108.82 627.359 1132.83C642.913 1153.78 658.494 1174.71 674.028 1195.67C674.805 1196.72 675.255 1198 675.929 1199.52C650.74 1212.17 625.519 1224.57 600.211 1236.78C566.531 1253.04 532.784 1269.16 499.062 1285.33C497.897 1285.89 496.693 1286.36 494.836 1287.17C494.836 1284.83 494.836 1283.06 494.836 1281.3C494.835 1222.2 494.876 1163.09 494.743 1103.99C494.734 1099.95 495.904 1097.89 499.628 1096.14C517.528 1087.75 535.256 1078.99 553.065 1070.4C559.743 1067.18 566.474 1064.07 573.537 1060.84Z" fill="#1E7DFD"/>
|
||||
<path d="M1012.99 849.188C1016.68 847.418 1020.36 845.648 1024.73 843.549C1018.48 840.456 1012.91 837.702 1007.07 834.349C1006.12 832.264 1005.67 830.604 1004.71 829.316C967.154 778.698 929.587 728.087 891.941 677.535C862.242 637.653 832.398 597.88 802.743 557.965C799.446 553.528 796.969 548.481 794.112 543.717C809.273 550.913 824.434 558.109 839.835 565.927C840.914 568.328 841.482 570.305 842.629 571.855C865.788 603.159 889 634.423 912.227 665.677C942.998 707.081 973.774 748.481 1004.58 789.861C1045.71 845.125 1086.9 900.348 1127.99 955.645C1132.92 962.283 1137.39 969.27 1142.07 976.095C1133.3 980.398 1124.54 984.701 1115.27 988.743C1114.35 987.757 1114.02 986.976 1113.53 986.315C1082.22 944.238 1050.88 902.187 1019.63 860.07C1017.11 856.672 1015.19 852.826 1012.99 849.188Z" fill="#5A8AF3"/>
|
||||
<path d="M1142.32 976.062C1137.39 969.27 1132.92 962.283 1127.99 955.645C1086.9 900.349 1045.71 845.125 1004.58 789.862C973.774 748.481 942.997 707.081 912.227 665.677C889 634.424 865.787 603.159 842.629 571.855C841.482 570.305 840.914 568.328 840.072 566.172C840.241 565.816 840.414 565.838 840.995 566.161C841.676 566.658 841.975 566.792 842.301 566.866C854.035 572.433 865.768 578 877.935 583.988C878.678 584.562 878.987 584.713 879.297 584.864C879.297 584.864 879.157 584.795 879.195 585.104C880.05 587.014 880.661 588.769 881.717 590.194C910.15 628.536 938.613 666.857 967.104 705.157C1007.41 759.336 1047.75 813.486 1088.06 867.667C1107.91 894.357 1127.68 921.108 1147.58 947.766C1149.09 949.788 1151.3 951.286 1153.18 953.028C1153.3 958.139 1153.63 963.259 1153.39 968.354C1153.33 969.717 1151.79 971.318 1150.51 972.244C1148.85 973.447 1146.79 974.086 1144.32 975.088C1143.47 975.349 1143.22 975.534 1143.01 975.763C1142.86 975.851 1142.72 975.94 1142.32 976.062Z" fill="#698AF1"/>
|
||||
<path d="M655 477.119C671.918 485.167 688.835 493.216 705.907 501.945C706.918 504.395 707.514 506.358 708.666 507.908C747.737 560.482 786.848 613.025 825.981 665.553C855.48 705.149 885.039 744.7 914.477 784.342C916.098 786.526 916.851 789.354 918.009 791.883C901.525 783.985 885.042 776.087 868.101 767.832C867.405 767.291 867.146 767.145 866.867 767.035C866.867 767.035 866.945 767.044 866.935 766.705C864.59 762.406 862.59 758.199 859.869 754.526C829.093 712.982 798.23 671.503 767.345 630.04C731.892 582.444 696.373 534.898 660.961 487.271C658.633 484.141 656.972 480.515 655 477.119Z" fill="#2E8CF7"/>
|
||||
<path d="M918.304 792.078C916.851 789.354 916.098 786.526 914.476 784.342C885.039 744.7 855.48 705.149 825.981 665.553C786.848 613.025 747.737 560.482 708.666 507.908C707.514 506.358 706.918 504.396 706.052 502.254C713.533 505.222 721.066 508.471 728.504 511.924C736.576 515.671 744.56 519.608 752.819 524.093C753.776 526.37 754.212 528.228 755.257 529.635C794.882 583.014 834.538 636.37 874.245 689.688C903.727 729.277 933.303 768.797 962.746 808.415C963.949 810.034 964.226 812.341 964.934 814.327C949.489 806.976 934.044 799.625 918.304 792.078Z" fill="#3F8BF5"/>
|
||||
<path d="M654.678 476.975C656.972 480.515 658.633 484.141 660.961 487.271C696.373 534.898 731.892 582.444 767.346 630.04C798.231 671.503 829.093 712.982 859.869 754.526C862.59 758.199 864.59 762.406 866.929 766.743C853.125 760.564 839.317 754.006 825.234 746.853C823.972 744.365 823.219 742.299 821.962 740.608C804.601 717.249 787.146 693.962 769.764 670.619C732.418 620.466 695.095 570.296 657.776 520.123C643.483 500.907 629.192 481.691 615.023 462.385C613.942 460.912 613.842 458.719 613.286 456.86C626.976 463.517 640.666 470.175 654.678 476.975Z" fill="#1D8DF9"/>
|
||||
<path d="M1153.31 952.573C1151.3 951.286 1149.09 949.788 1147.58 947.766C1127.68 921.108 1107.91 894.357 1088.06 867.667C1047.75 813.486 1007.41 759.336 967.104 705.156C938.613 666.857 910.15 628.536 881.717 590.194C880.661 588.769 880.05 587.014 879.233 585.036C891.058 590.238 902.881 595.816 914.729 601.9C915.531 604.143 916.034 606.081 917.129 607.588C925.338 618.891 933.626 630.138 941.96 641.35C972.813 682.862 1003.68 724.367 1034.57 765.849C1072.42 816.668 1110.29 867.47 1148.23 918.22C1149.42 919.823 1151.5 920.768 1153.17 922.02C1153.26 932.052 1153.35 942.085 1153.31 952.573Z" fill="#7589F0"/>
|
||||
<path d="M965.163 814.57C964.226 812.341 963.949 810.034 962.746 808.415C933.303 768.797 903.727 729.277 874.245 689.688C834.538 636.37 794.882 583.014 755.257 529.635C754.212 528.228 753.776 526.37 753.048 524.353C765.367 529.953 777.69 535.933 790.029 541.878C791.172 542.428 792.417 542.765 793.863 543.459C796.969 548.481 799.446 553.528 802.743 557.965C832.397 597.88 862.242 637.653 891.941 677.535C929.586 728.087 967.153 778.698 1004.71 829.316C1005.67 830.604 1006.12 832.264 1006.82 834.136C993.022 827.953 979.207 821.383 965.163 814.57Z" fill="#4C8BF4"/>
|
||||
<path d="M1153.55 884.024C1153.49 886.064 1153.44 888.104 1152.92 890.507C1151.15 889.673 1149.6 888.651 1148.56 887.251C1113.8 840.547 1079.14 793.777 1044.37 747.081C1014.67 707.19 984.833 667.397 955.172 627.477C953.588 625.345 953.058 622.429 952.038 619.878C967.238 626.986 982.438 634.094 997.901 641.832C999.291 644.658 1000.15 647.054 1001.59 649.02C1011.81 662.954 1022.12 676.818 1032.45 690.672C1071.27 742.759 1110.1 794.841 1149 846.875C1150 848.216 1151.77 848.984 1153.18 850.019C1153.3 860.672 1153.42 871.325 1153.43 882.613C1153.24 883.552 1153.32 883.81 1153.55 884.024Z" fill="#8D88EE"/>
|
||||
<path d="M951.884 619.568C953.059 622.429 953.588 625.344 955.172 627.476C984.834 667.397 1014.67 707.19 1044.37 747.081C1079.14 793.777 1113.8 840.547 1148.56 887.251C1149.6 888.651 1151.15 889.673 1152.81 890.932C1153.25 901.034 1153.33 911.074 1153.3 921.567C1151.5 920.768 1149.42 919.823 1148.23 918.22C1110.29 867.47 1072.42 816.668 1034.57 765.849C1003.68 724.367 972.814 682.862 941.96 641.35C933.626 630.138 925.338 618.891 917.129 607.588C916.034 606.081 915.531 604.143 914.948 602.174C927.338 607.714 939.534 613.486 951.884 619.568Z" fill="#8089EF"/>
|
||||
<path d="M613.054 456.589C613.842 458.719 613.942 460.912 615.023 462.385C629.192 481.691 643.483 500.907 657.776 520.123C695.095 570.296 732.418 620.466 769.764 670.62C787.146 693.962 804.601 717.25 821.962 740.608C823.219 742.299 823.972 744.365 824.981 746.62C812.766 741.194 800.53 735.407 788.068 729.351C787.339 727.059 787.39 724.628 786.248 723.073C772.664 704.581 758.891 686.227 745.189 667.821C706.863 616.338 668.539 564.853 630.232 513.356C613.262 490.542 596.319 467.709 579.422 444.841C578.37 443.418 577.825 441.62 577.017 439.646C588.933 444.971 600.878 450.644 613.054 456.589Z" fill="#128DFA"/>
|
||||
<path d="M1153.3 849.564C1151.77 848.984 1150 848.215 1149 846.875C1110.1 794.841 1071.27 742.759 1032.45 690.672C1022.12 676.817 1011.81 662.954 1001.59 649.02C1000.15 647.054 999.291 644.658 998.123 642.089C1012.63 648.603 1027.18 655.476 1041.71 662.381C1046.66 664.73 1051.57 667.158 1056.75 670.17C1057.7 672.424 1058.09 674.275 1059.12 675.662C1088.63 715.51 1118.18 755.326 1147.83 795.068C1149.1 796.762 1151.37 797.705 1153.17 798.998C1153.25 815.701 1153.34 832.405 1153.3 849.564Z" fill="#9F87EC"/>
|
||||
<path d="M1012.65 849.228C1015.19 852.826 1017.11 856.672 1019.63 860.07C1050.88 902.187 1082.22 944.238 1113.53 986.315C1114.02 986.976 1114.35 987.757 1114.91 988.795C1103.99 994.487 1092.92 999.865 1081.41 1004.97C1080.17 1003.25 1079.53 1001.69 1078.56 1000.37C1053.24 966.251 1027.87 932.166 1002.56 898.045C994.651 887.391 986.857 876.655 979.012 865.957C988.519 861.278 998.031 856.61 1007.53 851.908C1009.16 851.102 1010.71 850.151 1012.65 849.228Z" fill="#448BF5"/>
|
||||
<path d="M978.649 866.002C986.857 876.656 994.651 887.392 1002.56 898.045C1027.87 932.166 1053.24 966.252 1078.56 1000.37C1079.53 1001.69 1080.17 1003.25 1081.05 1005.07C1072.47 1009.66 1063.81 1013.87 1054.47 1017.96C1052.78 1016.77 1051.65 1015.78 1050.77 1014.6C1022.68 977.014 994.565 939.443 966.575 901.782C961.14 894.471 956.193 886.797 951.021 879.29C960.11 874.876 969.199 870.461 978.649 866.002Z" fill="#2E8CF7"/>
|
||||
<path d="M950.686 879.365C956.194 886.797 961.141 894.471 966.575 901.782C994.565 939.443 1022.68 977.014 1050.77 1014.6C1051.65 1015.78 1052.78 1016.77 1054.13 1018.02C1045.59 1022.5 1036.72 1026.81 1027.51 1031.28C1021.33 1023.01 1015.71 1014.41 1009.59 1006.18C981.974 968.983 954.214 931.89 926.518 894.75C926.025 894.089 925.693 893.309 925.135 892.276C933.44 887.792 941.895 883.616 950.686 879.365Z" fill="#1D8DF9"/>
|
||||
<path d="M1153.28 798.541C1151.37 797.705 1149.1 796.762 1147.83 795.068C1118.18 755.326 1088.63 715.51 1059.12 675.662C1058.1 674.275 1057.7 672.424 1057 670.411C1068.78 675.637 1080.57 681.243 1092.67 687.412C1093.6 689.46 1093.97 691.138 1094.9 692.398C1112.7 716.52 1130.55 740.612 1148.5 764.63C1149.59 766.098 1151.59 766.9 1153.16 768.012C1153.24 778.036 1153.32 788.06 1153.28 798.541Z" fill="#AC86E8"/>
|
||||
<path d="M1153.28 767.556C1151.59 766.9 1149.59 766.098 1148.5 764.63C1130.55 740.612 1112.7 716.52 1094.9 692.398C1093.97 691.138 1093.6 689.46 1092.96 687.611C1101.77 691.445 1110.58 695.646 1119.72 700.394C1120.5 701.995 1120.79 703.176 1121.45 704.08C1130.61 716.553 1139.78 729.017 1149.08 741.384C1149.99 742.595 1151.77 743.155 1153.14 744.018C1153.23 751.711 1153.32 759.405 1153.28 767.556Z" fill="#B77ED4"/>
|
||||
<path d="M1153.26 743.563C1151.77 743.155 1149.99 742.595 1149.08 741.384C1139.78 729.017 1130.61 716.553 1121.45 704.08C1120.79 703.176 1120.5 701.994 1120.01 700.594C1130.33 705.134 1140.71 709.92 1150.94 715.04C1152.24 715.693 1153.35 718.074 1153.39 719.686C1153.6 727.487 1153.42 735.3 1153.26 743.563Z" fill="#BC79C9"/>
|
||||
<path d="M1332.71 97.635C1334.63 97.4262 1336.93 97.361 1339.54 97.4188C1337.6 97.6208 1335.35 97.6997 1332.71 97.635Z" fill="#4D6971"/>
|
||||
<path d="M1549.3 1305.47C1549.48 1305.91 1549.56 1306.67 1549.52 1307.64C1549.33 1307.16 1549.27 1306.48 1549.3 1305.47Z" fill="#CA6DAB"/>
|
||||
<path d="M1549.31 514.353C1549.39 513.997 1549.7 514.031 1549.86 514.059C1549.9 514.849 1549.78 515.611 1549.53 516.628C1549.33 516.159 1549.28 515.434 1549.31 514.353Z" fill="#E45672"/>
|
||||
<path d="M1207.27 97.6518C1207.52 97.4595 1207.98 97.3837 1208.66 97.436C1208.4 97.6321 1207.94 97.7002 1207.27 97.6518Z" fill="#4D6971"/>
|
||||
<path d="M842.16 566.643C841.975 566.792 841.676 566.658 841.216 566.301C841.359 566.232 841.689 566.326 842.16 566.643Z" fill="#5A8AF3"/>
|
||||
<path d="M994.094 97.6683C994.163 97.4895 994.333 97.4307 994.757 97.4601C995.011 97.5481 994.655 97.8216 994.655 97.8216C994.655 97.8216 994.195 97.7884 994.094 97.6683Z" fill="#4D6971"/>
|
||||
<path d="M1204.26 97.5847C1204.23 97.2894 1204.48 97.2222 1204.87 97.4235C1204.97 97.524 1204.5 97.6451 1204.26 97.5847Z" fill="#4D6971"/>
|
||||
<path d="M1211.19 97.669C1211.11 97.5729 1211.5 97.4286 1211.7 97.5007C1211.89 97.5728 1211.62 97.7914 1211.62 97.7914C1211.62 97.7914 1211.26 97.7651 1211.19 97.669Z" fill="#4D6971"/>
|
||||
<path d="M1346.18 97.6282C1346.19 97.3699 1346.34 97.3115 1346.62 97.4616C1346.7 97.5562 1346.27 97.7142 1346.18 97.6282Z" fill="#4D6971"/>
|
||||
<path d="M1354.45 97.7391C1354.45 97.6876 1354.58 97.5703 1354.57 97.6325C1354.56 97.6948 1354.44 97.7906 1354.45 97.7391Z" fill="#4D6971"/>
|
||||
<path d="M1549.32 1297.31C1549.57 1297.29 1549.62 1297.48 1549.46 1297.79C1549.39 1297.87 1549.26 1297.5 1549.32 1297.31Z" fill="#CA6DAB"/>
|
||||
<path d="M879.169 584.614C878.987 584.713 878.678 584.562 878.176 584.237C878.336 584.164 878.688 584.264 879.169 584.614Z" fill="#7589F0"/>
|
||||
<path d="M1549.62 743.518C1549.78 743.693 1549.7 744.04 1549.49 744.625C1549.37 744.471 1549.37 744.08 1549.62 743.518Z" fill="#D9608B"/>
|
||||
<path d="M866.98 767.249C867.146 767.145 867.405 767.291 867.808 767.625C867.623 767.832 867.33 767.727 866.98 767.249Z" fill="#1D8DF9"/>
|
||||
<path d="M1153.66 883.848C1153.32 883.81 1153.24 883.552 1153.44 883.075C1153.78 883.117 1153.85 883.374 1153.66 883.848Z" fill="#4C6972"/>
|
||||
<path d="M1143.25 975.744C1143.22 975.534 1143.47 975.349 1143.96 975.123C1144.04 975.354 1143.81 975.583 1143.25 975.744Z" fill="#5A8AF3"/>
|
||||
<path d="M1549.32 1313.31C1549.57 1313.29 1549.62 1313.48 1549.46 1313.79C1549.39 1313.87 1549.26 1313.5 1549.32 1313.31Z" fill="#CA6DAB"/>
|
||||
<path d="M1549.32 1310.31C1549.57 1310.29 1549.62 1310.48 1549.46 1310.79C1549.39 1310.87 1549.26 1310.5 1549.32 1310.31Z" fill="#CA6DAB"/>
|
||||
<path d="M1549.32 1301.31C1549.57 1301.29 1549.62 1301.48 1549.46 1301.79C1549.39 1301.87 1549.26 1301.5 1549.32 1301.31Z" fill="#CA6DAB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_117_117">
|
||||
<rect width="1645" height="1645" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 47 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="0 0 1200 1200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="1200" rx="260" fill="#9046FF"/>
|
||||
<mask id="mask0" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="272" y="202" width="655" height="796">
|
||||
<path d="M926.578 202.793H272.637V997.857H926.578V202.793Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M398.554 818.914C316.315 1001.03 491.477 1046.74 620.672 940.156C658.687 1059.66 801.052 970.473 852.234 877.795C964.787 673.567 919.318 465.357 907.64 422.374C827.637 129.443 427.623 128.946 358.8 423.865C342.651 475.544 342.402 534.18 333.458 595.051C328.986 625.86 325.507 645.488 313.83 677.785C306.873 696.424 297.68 712.819 282.773 740.645C259.915 783.881 269.604 867.113 387.87 823.883L399.051 818.914H398.554Z" fill="white"/>
|
||||
<path d="M636.123 549.353C603.328 549.353 598.359 510.097 598.359 486.742C598.359 465.623 602.086 448.977 609.293 438.293C615.504 428.852 624.697 424.131 636.123 424.131C647.555 424.131 657.492 428.852 664.447 438.541C672.398 449.474 676.623 466.12 676.623 486.742C676.623 525.998 661.471 549.353 636.375 549.353H636.123Z" fill="black"/>
|
||||
<path d="M771.24 549.353C738.445 549.353 733.477 510.097 733.477 486.742C733.477 465.623 737.203 448.977 744.41 438.293C750.621 428.852 759.814 424.131 771.24 424.131C782.672 424.131 792.609 428.852 799.564 438.541C807.516 449.474 811.74 466.12 811.74 486.742C811.74 525.998 796.588 549.353 771.492 549.353H771.24Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="#10A37F"/>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,41 +0,0 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d)">
|
||||
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:overlay" opacity="0.25">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -87,7 +87,6 @@
|
||||
"components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx": 1,
|
||||
"components/interfaces/Settings/Database/SSLConfiguration.tsx": 1,
|
||||
"components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx": 1,
|
||||
"components/interfaces/Settings/Logs/LogTable.tsx": 2,
|
||||
"components/interfaces/Settings/Logs/LogsPreviewer.tsx": 2,
|
||||
@@ -948,7 +947,6 @@
|
||||
"components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx": 1,
|
||||
"components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx": 1,
|
||||
"components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1,
|
||||
"components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx": 1,
|
||||
@@ -1110,7 +1108,6 @@
|
||||
"components/interfaces/Organization/Usage/UsageBarChart.tsx": 1,
|
||||
"components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx": 1,
|
||||
"components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1,
|
||||
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
|
||||
"components/interfaces/Settings/Logs/LogSelection.tsx": 1,
|
||||
"components/interfaces/Settings/Logs/LogTable.tsx": 3,
|
||||
"components/interfaces/Settings/Logs/LogsFormatters.tsx": 2,
|
||||
|
||||
@@ -77,7 +77,6 @@ export const SupabaseGrid = ({
|
||||
} = useTableRowsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId,
|
||||
sorts,
|
||||
filters,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import type { RenderEditCellProps } from 'react-data-grid'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import * as React from 'react'
|
||||
import type { RenderEditCellProps } from 'react-data-grid'
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
@@ -29,15 +29,18 @@ function BaseEditor<TRow, TSummaryRow = unknown>({
|
||||
onClose,
|
||||
}: TimeEditorProps<TRow, TSummaryRow>) {
|
||||
const value = row[column.key as keyof TRow] as unknown as string
|
||||
const timeValue = value ? dayjs(value, format).format(INPUT_TIME_FORMAT) : value
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const _value = event.target.value
|
||||
|
||||
if (_value == '') {
|
||||
onRowChange({ ...row, [column.key]: null })
|
||||
} else {
|
||||
const _timeValue = dayjs(_value, INPUT_TIME_FORMAT).format(format)
|
||||
onRowChange({ ...row, [column.key]: _timeValue })
|
||||
const dayJsValue = dayjs(_value, format)
|
||||
if (dayJsValue.isValid()) {
|
||||
const _timeValue = dayJsValue.format(format)
|
||||
onRowChange({ ...row, [column.key]: _timeValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ function BaseEditor<TRow, TSummaryRow = unknown>({
|
||||
<input
|
||||
className="sb-grid-time-editor"
|
||||
ref={autoFocusAndSelect}
|
||||
value={timeValue ?? ''}
|
||||
defaultValue={value ?? ''}
|
||||
onChange={onChange}
|
||||
onBlur={() => onClose(true)}
|
||||
type="time"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
|
||||
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { RoleImpersonationState } from 'lib/role-impersonation'
|
||||
import { ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react'
|
||||
import { AlertCircle, ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
@@ -90,7 +90,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
} = useTableRowsCountQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
filters,
|
||||
enforceExactCount: snap.enforceExactCount,
|
||||
@@ -102,6 +101,8 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
}
|
||||
)
|
||||
const count = data?.count ?? 0
|
||||
const hasCountData = count >= 0
|
||||
const isEstimateCount = data?.is_estimate ?? false
|
||||
const countString = data?.is_estimate ? formatEstimatedCount(count) : count.toLocaleString()
|
||||
const maxPages = Math.ceil(count / tableEditorSnap.rowsPerPage)
|
||||
const totalPages = count > 0 ? maxPages : 1
|
||||
@@ -113,7 +114,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
const { data: rowsData, isPending: isLoadingRows } = useTableRowsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: id,
|
||||
sorts,
|
||||
filters,
|
||||
@@ -144,12 +144,10 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
}
|
||||
|
||||
const onNextPage = () => {
|
||||
if (page < maxPages) {
|
||||
if (snap.selectedRows.size >= 1) {
|
||||
setIsConfirmNextModalOpen(true)
|
||||
} else {
|
||||
goToNextPage()
|
||||
}
|
||||
if (snap.selectedRows.size >= 1) {
|
||||
setIsConfirmNextModalOpen(true)
|
||||
} else {
|
||||
goToNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +174,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
const onRowsPerPageChange = (value: string | number) => {
|
||||
const rowsPerPage = Number(value)
|
||||
tableEditorSnap.setRowsPerPage(isNaN(rowsPerPage) ? 100 : rowsPerPage)
|
||||
snap.setPage(1)
|
||||
}
|
||||
|
||||
// keep input value in-sync with actual page
|
||||
@@ -183,20 +182,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
setValue(String(page))
|
||||
}, [page])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isForeignTableSelected && page && page > totalPages) {
|
||||
snap.setPage(totalPages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isForeignTableSelected, page, totalPages])
|
||||
|
||||
useEffect(() => {
|
||||
if (id !== undefined) {
|
||||
snap.setEnforceExactCount(rowsCountEstimate !== null && rowsCountEstimate <= THRESHOLD_COUNT)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
// If the count query encountered a timeout error with exact count
|
||||
// turn off the exact count to rely on approximate
|
||||
@@ -206,6 +191,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isError, snap.enforceExactCount, error?.code])
|
||||
|
||||
// [Joshen] One to revisit if we can consolidate this and the main return statement
|
||||
if (isForeignTableSelected) {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
@@ -251,102 +237,107 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-4">
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
<p className="text-xs text-foreground-light">Loading records count...</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
aria-label="Previous page"
|
||||
icon={<ArrowLeft />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
disabled={page <= 1 || isLoading}
|
||||
onClick={onPreviousPage}
|
||||
/>
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xs text-foreground-light">Page</p>
|
||||
|
||||
<Input
|
||||
className="w-12"
|
||||
size="tiny"
|
||||
min={1}
|
||||
max={isSuccess ? maxPages : undefined}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
const parsedValue = Number(value)
|
||||
if (
|
||||
(e.code === 'Enter' || e.code === 'NumpadEnter') &&
|
||||
!Number.isNaN(parsedValue) &&
|
||||
parsedValue >= 1
|
||||
) {
|
||||
onPageChange(parsedValue)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{isSuccess && hasCountData && (
|
||||
<p className="text-xs text-foreground-light">of {totalPages.toLocaleString()}</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
aria-label="Next page"
|
||||
icon={<ArrowRight />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
disabled={isLastPage}
|
||||
onClick={onNextPage}
|
||||
/>
|
||||
|
||||
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Button type="text" className="w-7" icon={<Loader2 size={12} className="animate-spin" />} />
|
||||
) : isError ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label="Previous page"
|
||||
icon={<ArrowLeft />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
disabled={page <= 1 || isLoading}
|
||||
onClick={onPreviousPage}
|
||||
/>
|
||||
<p className="text-xs text-foreground-light">Page</p>
|
||||
<Input
|
||||
className="w-12"
|
||||
size="tiny"
|
||||
min={1}
|
||||
max={maxPages}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
const parsedValue = Number(value)
|
||||
if (
|
||||
(e.code === 'Enter' || e.code === 'NumpadEnter') &&
|
||||
!Number.isNaN(parsedValue) &&
|
||||
parsedValue >= 1 &&
|
||||
parsedValue <= maxPages
|
||||
) {
|
||||
onPageChange(parsedValue)
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
className="w-7"
|
||||
loading={isFetching}
|
||||
icon={<AlertCircle />}
|
||||
/>
|
||||
|
||||
<p className="text-xs text-foreground-light">of {totalPages.toLocaleString()}</p>
|
||||
|
||||
<Button
|
||||
aria-label="Next page"
|
||||
icon={<ArrowRight />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
disabled={page >= maxPages || isLoading}
|
||||
onClick={onNextPage}
|
||||
/>
|
||||
|
||||
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
|
||||
</div>
|
||||
|
||||
{!isForeignTableSelected && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xs text-foreground-light">
|
||||
{`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
|
||||
{data.is_estimate ? '(estimated)' : ''}
|
||||
</p>
|
||||
|
||||
{data.is_estimate && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
className="px-1.5"
|
||||
loading={isFetching}
|
||||
icon={<HelpCircle />}
|
||||
onClick={() => {
|
||||
// Show warning if either NOT a table entity, or table rows estimate is beyond threshold
|
||||
if (rowsCountEstimate === null || count > THRESHOLD_COUNT) {
|
||||
setIsConfirmFetchExactCountModalOpen(true)
|
||||
} else snap.setEnforceExactCount(true)
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="w-72">
|
||||
This is an estimated value as your table has more than{' '}
|
||||
{THRESHOLD_COUNT.toLocaleString()} rows. <br />
|
||||
<span className="text-brand">
|
||||
Click to retrieve the exact count of the table.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Failed to retrieve count: {error?.message}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : !isForeignTableSelected ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
{hasCountData && (
|
||||
<p className="text-xs text-foreground-light">
|
||||
{`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
|
||||
{data.is_estimate ? '(estimated)' : ''}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Error fetching records count. Please refresh the page.
|
||||
</p>
|
||||
)}
|
||||
{data.is_estimate && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
className="w-7"
|
||||
loading={isFetching}
|
||||
icon={<HelpCircle />}
|
||||
onClick={() => {
|
||||
// Show warning if either NOT a table entity, or table rows estimate is beyond threshold
|
||||
if (rowsCountEstimate === null || count === -1 || count > THRESHOLD_COUNT) {
|
||||
setIsConfirmFetchExactCountModalOpen(true)
|
||||
} else {
|
||||
snap.setEnforceExactCount(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="w-72">
|
||||
{hasCountData
|
||||
? `This is an estimated value as your table has more than ${THRESHOLD_COUNT.toLocaleString()} rows.`
|
||||
: `Count not automatically loaded as your table has more than ${THRESHOLD_COUNT.toLocaleString()} rows.`}{' '}
|
||||
<br />
|
||||
<span className="text-brand">Click to retrieve the exact count of the table.</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isConfirmPreviousModalOpen}
|
||||
|
||||
@@ -39,7 +39,6 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
|
||||
} = useTableRowsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: table.id,
|
||||
filters: [{ column, operator: '=', value }],
|
||||
page: 1,
|
||||
|
||||
@@ -113,7 +113,6 @@ export const Grid = memo(
|
||||
|
||||
const { data } = useForeignKeyConstraintsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: table?.schema ?? undefined,
|
||||
})
|
||||
|
||||
|
||||
@@ -245,7 +245,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
|
||||
const { data } = useTableRowsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
sorts,
|
||||
filters,
|
||||
@@ -260,7 +259,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
|
||||
const { data: countData } = useTableRowsCountQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
filters,
|
||||
enforceExactCount: snap.enforceExactCount,
|
||||
|
||||
@@ -246,7 +246,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
|
||||
const { data } = useTableRowsQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
sorts,
|
||||
filters,
|
||||
@@ -261,7 +260,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
|
||||
const { data: countData } = useTableRowsCountQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
filters,
|
||||
enforceExactCount: snap.enforceExactCount,
|
||||
|
||||
@@ -50,11 +50,13 @@ const DATE_OPERATORS: OperatorDefinition[] = [
|
||||
{ value: '<=', label: 'Less or equal', group: 'comparison' },
|
||||
{ value: '=', label: 'Equals', group: 'comparison' },
|
||||
{ value: '<>', label: 'Not equal', group: 'comparison' },
|
||||
{ value: 'is', label: 'Is', group: 'setNull' },
|
||||
]
|
||||
|
||||
const BOOLEAN_OPERATORS: OperatorDefinition[] = [
|
||||
{ value: '=', label: 'Equals', group: 'comparison' },
|
||||
{ value: '<>', label: 'Not equal', group: 'comparison' },
|
||||
{ value: 'is', label: 'Is', group: 'setNull' },
|
||||
]
|
||||
|
||||
export function columnToFilterProperty(column: SupaColumn): FilterProperty {
|
||||
|
||||
@@ -10,16 +10,16 @@ import { isEqual } from 'lodash'
|
||||
import { ChevronDown, List } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
type RoleImpersonationState,
|
||||
useRoleImpersonationStateSnapshot,
|
||||
type RoleImpersonationState,
|
||||
} from 'state/role-impersonation-state'
|
||||
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
|
||||
import {
|
||||
Button,
|
||||
Popover_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverSeparator_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
@@ -72,7 +72,6 @@ export const SortPopoverPrimitive = ({
|
||||
const { data: countData } = useTableRowsCountQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
tableId: snap.table.id,
|
||||
filters,
|
||||
enforceExactCount: snap.enforceExactCount,
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { Database } from 'lucide-react'
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Auth, EdgeFunctions, Storage } from 'icons'
|
||||
import { Database } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import type { ICommand } from 'ui-patterns/CommandMenu'
|
||||
import {
|
||||
CommandHeader,
|
||||
CommandInput,
|
||||
CommandWrapper,
|
||||
PageType,
|
||||
useQuery,
|
||||
useRegisterCommands,
|
||||
useRegisterPage,
|
||||
useSetPage,
|
||||
useQuery,
|
||||
} from 'ui-patterns/CommandMenu'
|
||||
|
||||
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { orderCommandSectionsByPriority } from './ordering'
|
||||
import { ContextSearchResults } from './ContextSearchResults'
|
||||
import { useFlag, IS_PLATFORM } from 'common'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { orderCommandSectionsByPriority } from './ordering'
|
||||
import type { SearchContextValue } from './SearchContext.types'
|
||||
|
||||
interface SearchContextOption {
|
||||
@@ -81,7 +82,6 @@ function ContextSearchPage({
|
||||
}
|
||||
|
||||
export function useContextSearchCommands() {
|
||||
const enableSearchEntitiesCommandMenu = useFlag('enableSearchEntitiesCommandMenu')
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const setPage = useSetPage()
|
||||
|
||||
@@ -147,6 +147,6 @@ export function useContextSearchCommands() {
|
||||
useRegisterCommands(COMMAND_MENU_SECTIONS.QUERY, contextCommands, {
|
||||
orderSection: orderCommandSectionsByPriority,
|
||||
sectionMeta: { priority: 3 },
|
||||
enabled: !IS_PLATFORM || (enableSearchEntitiesCommandMenu && !!project),
|
||||
enabled: !IS_PLATFORM || !!project,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { IS_PLATFORM, useFlag } from 'common'
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
||||
import {
|
||||
Clock5,
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
} from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useMemo } from 'react'
|
||||
import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
|
||||
import {
|
||||
PageType,
|
||||
useRegisterCommands,
|
||||
useRegisterPage,
|
||||
useSetCommandMenuOpen,
|
||||
} from 'ui-patterns/CommandMenu'
|
||||
import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
|
||||
|
||||
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
|
||||
import {
|
||||
@@ -51,14 +51,12 @@ const Graphql = dynamic(() => import('icons').then((mod) => mod.Graphql))
|
||||
const CREATE_STUDIO_ENTITY = 'Create Studio Entity'
|
||||
|
||||
export function useCreateCommands(options?: CommandOptions) {
|
||||
const enableCreateCommands = useFlag('enablecreatecommands')
|
||||
const setIsOpen = useSetCommandMenuOpen()
|
||||
const {
|
||||
ref,
|
||||
setPage,
|
||||
openSidebar,
|
||||
snap,
|
||||
authenticationOauth21,
|
||||
authEnabled,
|
||||
edgeFunctionsEnabled,
|
||||
storageEnabled,
|
||||
@@ -184,7 +182,7 @@ export function useCreateCommands(options?: CommandOptions) {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(IS_PLATFORM && authenticationOauth21
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
id: 'create-oauth-app',
|
||||
@@ -207,7 +205,6 @@ export function useCreateCommands(options?: CommandOptions) {
|
||||
passwordVerificationHook,
|
||||
passwordVerificationHookEnabled,
|
||||
beforeUserCreatedHook,
|
||||
authenticationOauth21,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -453,7 +450,7 @@ export function useCreateCommands(options?: CommandOptions) {
|
||||
},
|
||||
{
|
||||
deps: [sections],
|
||||
enabled: enableCreateCommands,
|
||||
enabled: true,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -471,7 +468,7 @@ export function useCreateCommands(options?: CommandOptions) {
|
||||
...options,
|
||||
orderSection: (sections) => sections,
|
||||
sectionMeta: { priority: 3 },
|
||||
enabled: enableCreateCommands,
|
||||
enabled: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { useParams } from 'common'
|
||||
import type { Hook } from 'components/interfaces/Auth/Hooks/hooks.constants'
|
||||
import { HOOKS_DEFINITIONS } from 'components/interfaces/Auth/Hooks/hooks.constants'
|
||||
import { extractMethod, isValidHook } from 'components/interfaces/Auth/Hooks/hooks.utils'
|
||||
@@ -74,7 +74,7 @@ export function getIntegrationCommandName(integration: IntegrationDefinition): s
|
||||
case 'cron':
|
||||
return 'Create Cron Job'
|
||||
case 'webhooks':
|
||||
return 'Create Webhook'
|
||||
return 'Create Database Webhook'
|
||||
case 'queues':
|
||||
return 'Create Queue'
|
||||
default:
|
||||
@@ -89,9 +89,6 @@ export function useCreateCommandsConfig() {
|
||||
const { openSidebar } = useSidebarManagerSnapshot()
|
||||
const snap = useAiAssistantStateSnapshot()
|
||||
|
||||
// Auth
|
||||
const authenticationOauth21 = useFlag('EnableOAuth21')
|
||||
|
||||
const {
|
||||
projectAuthAll: authEnabled,
|
||||
projectEdgeFunctionAll: edgeFunctionsEnabled,
|
||||
@@ -182,7 +179,6 @@ export function useCreateCommandsConfig() {
|
||||
setPage,
|
||||
openSidebar,
|
||||
snap,
|
||||
authenticationOauth21,
|
||||
authEnabled,
|
||||
edgeFunctionsEnabled,
|
||||
storageEnabled,
|
||||
|
||||
@@ -82,8 +82,9 @@ export const useIsColumnLevelPrivilegesEnabled = () => {
|
||||
|
||||
export const useUnifiedLogsPreview = () => {
|
||||
const { flags, onUpdateFlag } = useFeaturePreviewContext()
|
||||
const unifiedLogsEnabled = useFlag('unifiedLogs')
|
||||
|
||||
const isEnabled = flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
|
||||
const isEnabled = unifiedLogsEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
|
||||
|
||||
const enable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, true)
|
||||
const disable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, false)
|
||||
@@ -93,8 +94,7 @@ export const useUnifiedLogsPreview = () => {
|
||||
|
||||
export const useIsBranching2Enabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const gitlessBranchingEnabled = useFlag('gitlessBranching')
|
||||
return gitlessBranchingEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
|
||||
}
|
||||
|
||||
export const useIsPgDeltaDiffEnabled = () => {
|
||||
@@ -105,8 +105,7 @@ export const useIsPgDeltaDiffEnabled = () => {
|
||||
|
||||
export const useIsAdvisorRulesEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const advisorRulesEnabled = useFlag('advisorRules')
|
||||
return advisorRulesEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
|
||||
}
|
||||
|
||||
export const useIsQueueOperationsEnabled = () => {
|
||||
@@ -114,6 +113,12 @@ export const useIsQueueOperationsEnabled = () => {
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS]
|
||||
}
|
||||
|
||||
export const useIsPlatformWebhooksEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const platformWebhooksEnabled = useFlag('platformWebhooks')
|
||||
return platformWebhooksEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]
|
||||
}
|
||||
|
||||
export const useIsTableFilterBarEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]
|
||||
|
||||
@@ -24,6 +24,7 @@ import { APISidePanelPreview } from './APISidePanelPreview'
|
||||
import { Branching2Preview } from './Branching2Preview'
|
||||
import { CLSPreview } from './CLSPreview'
|
||||
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
|
||||
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
|
||||
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
|
||||
import { QueueOperationsPreview } from './QueueOperationsPreview'
|
||||
import { TableFilterBarPreview } from './TableFilterBarPreview'
|
||||
@@ -42,6 +43,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: <UnifiedLogsPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS]: <QueueOperationsPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]: <TableFilterBarPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
|
||||
}
|
||||
|
||||
export const FeaturePreviewModal = () => {
|
||||
@@ -63,15 +65,17 @@ export const FeaturePreviewModal = () => {
|
||||
)
|
||||
|
||||
const { flags, onUpdateFlag } = featurePreviewContext
|
||||
const selectedFeature =
|
||||
featurePreviews.find((preview) => preview.key === selectedFeatureKey) ?? featurePreviews[0]
|
||||
const isSelectedFeatureEnabled = flags[selectedFeatureKey]
|
||||
const allFeaturePreviews = (
|
||||
IS_PLATFORM ? featurePreviews : featurePreviews.filter((x) => !x.isPlatformOnly)
|
||||
).filter((x) => x.enabled)
|
||||
|
||||
const allFeaturePreviews = IS_PLATFORM
|
||||
? featurePreviews
|
||||
: featurePreviews.filter((x) => !x.isPlatformOnly)
|
||||
const selectedFeature =
|
||||
allFeaturePreviews.find((preview) => preview.key === selectedFeatureKey) ??
|
||||
allFeaturePreviews[0]
|
||||
const isSelectedFeatureEnabled = flags[selectedFeature?.key]
|
||||
|
||||
const toggleFeature = () => {
|
||||
if (!selectedFeature) return
|
||||
onUpdateFlag(selectedFeature.key, !isSelectedFeatureEnabled)
|
||||
sendEvent({
|
||||
action: isSelectedFeatureEnabled ? 'feature_preview_disabled' : 'feature_preview_enabled',
|
||||
@@ -98,7 +102,7 @@ export const FeaturePreviewModal = () => {
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<DialogSection className="!p-0">
|
||||
{featurePreviews.length > 0 ? (
|
||||
{allFeaturePreviews.length > 0 ? (
|
||||
<div className="flex">
|
||||
<div>
|
||||
<ScrollArea className="h-[550px] w-[280px] border-r">
|
||||
@@ -111,7 +115,7 @@ export const FeaturePreviewModal = () => {
|
||||
onClick={() => selectFeaturePreview(feature.key)}
|
||||
className={cn(
|
||||
'flex items-center justify-between p-4 border-b cursor-pointer bg transition',
|
||||
selectedFeature.key === feature.key ? 'bg-surface-300' : 'bg-surface-100'
|
||||
selectedFeature?.key === feature.key ? 'bg-surface-300' : 'bg-surface-100'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-x-3">
|
||||
@@ -150,7 +154,7 @@ export const FeaturePreviewModal = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature.key]}
|
||||
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature?.key ?? '']}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useParams } from 'common'
|
||||
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
|
||||
export const PlatformWebhooksPreview = () => {
|
||||
const { slug = '_', ref = '_' } = useParams()
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground-light mb-4">
|
||||
Configure webhook endpoints and review deliveries from both project and organization
|
||||
settings pages.
|
||||
</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>
|
||||
Project scope:{' '}
|
||||
<InlineLink href={`/project/${ref}/settings/webhooks`}>Project Webhooks</InlineLink>
|
||||
</li>
|
||||
<li>
|
||||
Organization scope:{' '}
|
||||
<InlineLink href={`/org/${slug}/webhooks`}>Organization Webhooks</InlineLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,11 +14,10 @@ type FeaturePreview = {
|
||||
}
|
||||
|
||||
export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
const gitlessBranchingEnabled = useFlag('gitlessBranching')
|
||||
const advisorRulesEnabled = useFlag('advisorRules')
|
||||
const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs')
|
||||
const tableEditorNewFilterBar = useFlag('tableEditorNewFilterBar')
|
||||
const pgDeltaDiffEnabled = useFlag('pgdeltaDiff')
|
||||
const platformWebhooksEnabled = useFlag('platformWebhooks')
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -34,7 +33,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0,
|
||||
name: 'Branching via dashboard',
|
||||
discussionsUrl: 'https://github.com/orgs/supabase/discussions/branching-2-0',
|
||||
enabled: gitlessBranchingEnabled,
|
||||
enabled: true,
|
||||
isNew: false,
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
@@ -43,7 +42,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES,
|
||||
name: 'Disable Advisor rules',
|
||||
discussionsUrl: undefined,
|
||||
enabled: advisorRulesEnabled,
|
||||
enabled: true,
|
||||
isNew: false,
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
@@ -58,6 +57,15 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
isDefaultOptIn: true,
|
||||
enabled: pgDeltaDiffEnabled,
|
||||
},
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS,
|
||||
name: 'Platform webhooks',
|
||||
discussionsUrl: undefined,
|
||||
isNew: true,
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
enabled: platformWebhooksEnabled,
|
||||
},
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
|
||||
name: 'Project API documentation',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FeatureKey } from 'data/entitlements/entitlements-query'
|
||||
|
||||
export interface Enum {
|
||||
label: string
|
||||
value: string
|
||||
@@ -22,7 +24,7 @@ export interface Provider {
|
||||
descriptionOptional?: string
|
||||
units?: string
|
||||
isSecret?: boolean
|
||||
isPaid?: boolean
|
||||
entitlementKey?: FeatureKey
|
||||
link?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ResourceItem } from 'components/ui/Resource/ResourceItem'
|
||||
import type { components } from 'data/api'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useHasEntitlementAccess } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Check } from 'lucide-react'
|
||||
@@ -67,7 +68,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
)
|
||||
}
|
||||
|
||||
const isFreePlan = organization?.plan.id === 'free'
|
||||
const hasEntitlementAccess = useHasEntitlementAccess()
|
||||
|
||||
const INITIAL_VALUES = (() => {
|
||||
const initialValues: { [x: string]: string | boolean } = {}
|
||||
@@ -201,18 +202,17 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
)
|
||||
}
|
||||
|
||||
const { entitlementKey } = provider.properties[x]
|
||||
const hasAccess =
|
||||
entitlementKey == null || hasEntitlementAccess(entitlementKey)
|
||||
const properties = {
|
||||
...provider.properties[x],
|
||||
description:
|
||||
provider.properties[x].isPaid && isFreePlan
|
||||
? `${description} Only available on [Pro plan](/org/${organization.slug}/billing?panel=subscriptionPlan) and above.`
|
||||
: description,
|
||||
description: hasAccess
|
||||
? description
|
||||
: `${description} Only available on [Pro plan](/org/${organization?.slug}/billing?panel=subscriptionPlan) and above.`,
|
||||
}
|
||||
const isDisabledDueToPlan = properties.isPaid && isFreePlan
|
||||
const shouldDisable =
|
||||
properties.type === 'boolean'
|
||||
? isDisabledDueToPlan && !values[x]
|
||||
: isDisabledDueToPlan
|
||||
properties.type === 'boolean' ? !hasAccess && !values[x] : !hasAccess
|
||||
|
||||
return (
|
||||
<FormField
|
||||
|
||||
@@ -38,7 +38,7 @@ const PROVIDER_EMAIL = {
|
||||
'Rejects the use of known or easy to guess passwords on sign up or password change. Powered by the HaveIBeenPwned.org Pwned Passwords API.',
|
||||
type: 'boolean',
|
||||
link: `${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection`,
|
||||
isPaid: true,
|
||||
entitlementKey: 'auth.password_hibp',
|
||||
},
|
||||
PASSWORD_MIN_LENGTH: {
|
||||
title: 'Minimum password length',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useParams } from 'common'
|
||||
import { convertArgumentTypes } from 'components/interfaces/Database/Functions/Functions.utils'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import FunctionSelector from 'components/ui/FunctionSelector'
|
||||
@@ -9,6 +10,7 @@ import { AuthConfigResponse } from 'data/auth/auth-config-query'
|
||||
import { useAuthHooksUpdateMutation } from 'data/auth/auth-hooks-update-mutation'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import randomBytes from 'randombytes'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
@@ -156,7 +158,16 @@ export const CreateHookSheet = ({
|
||||
},
|
||||
})
|
||||
|
||||
const isDirty = form.formState.isDirty
|
||||
const values = form.watch()
|
||||
const {
|
||||
confirmOnClose,
|
||||
handleOpenChange,
|
||||
modalProps: discardChangesModalProps,
|
||||
} = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const statements = useMemo(() => {
|
||||
let permissionChanges: string[] = []
|
||||
@@ -270,7 +281,7 @@ export const CreateHookSheet = ({
|
||||
}, [authConfig, title, visible, definition])
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={() => onClose()}>
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
aria-describedby={undefined}
|
||||
size="lg"
|
||||
@@ -411,17 +422,20 @@ export const CreateHookSheet = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-72 w-full gap-3 flex flex-col">
|
||||
<p className="text-sm text-foreground-light px-5">
|
||||
The following statements will be executed on the selected function:
|
||||
</p>
|
||||
<CodeEditor
|
||||
id="postgres-hook-editor"
|
||||
isReadOnly={true}
|
||||
language="pgsql"
|
||||
value={statements.join('\n\n')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{statements.length > 0 && (
|
||||
<div className="h-72 w-full gap-3 flex flex-col">
|
||||
<p className="text-sm text-foreground-light px-5">
|
||||
The following statements will be executed on the selected function:
|
||||
</p>
|
||||
<CodeEditor
|
||||
isReadOnly
|
||||
id="postgres-hook-editor"
|
||||
language="pgsql"
|
||||
value={statements.join('\n\n')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 px-5">
|
||||
@@ -470,7 +484,9 @@ export const CreateHookSheet = ({
|
||||
className="rounded-l-none text-xs"
|
||||
onClick={() => {
|
||||
const authHookSecret = generateAuthHookSecret()
|
||||
form.setValue('httpsValues.secret', authHookSecret)
|
||||
form.setValue('httpsValues.secret', authHookSecret, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Generate secret
|
||||
@@ -494,7 +510,7 @@ export const CreateHookSheet = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button disabled={isUpdatingAuthHooks} type="default" onClick={() => onClose()}>
|
||||
<Button disabled={isUpdatingAuthHooks} type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -507,6 +523,7 @@ export const CreateHookSheet = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
<DiscardChangesConfirmationDialog {...discardChangesModalProps} />
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -165,6 +165,7 @@ export const HooksListing = () => {
|
||||
variant="destructive"
|
||||
loading={isDeletingAuthHook}
|
||||
title={`Confirm to delete ${selectedHookForDeletion?.title}`}
|
||||
className={cn('md:px-0', selectedHookForDeletion?.method.type === 'postgres' && 'pb-0')}
|
||||
confirmLabel="Delete"
|
||||
confirmLabelLoading="Deleting"
|
||||
onCancel={() => setSelectedHookForDeletion(null)}
|
||||
@@ -181,20 +182,20 @@ export const HooksListing = () => {
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
<p className="md:px-5 text-sm text-foreground-light">
|
||||
Are you sure you want to delete the {selectedHookForDeletion?.title}?
|
||||
</p>
|
||||
{selectedHookForDeletion?.method.type === 'postgres' && (
|
||||
<>
|
||||
<p className="text-sm text-foreground-light">
|
||||
<p className="md:px-5 text-sm text-foreground-light">
|
||||
The following statements will be executed on the{' '}
|
||||
{selectedHookForDeletion?.method.schema}.
|
||||
{selectedHookForDeletion?.method.functionName} function:
|
||||
</p>
|
||||
<div className={cn('mt-4', 'h-72')}>
|
||||
<div className="mt-4 h-72">
|
||||
<CodeEditor
|
||||
isReadOnly
|
||||
id="deletion-hook-editor"
|
||||
isReadOnly={true}
|
||||
language="pgsql"
|
||||
value={getRevokePermissionStatements(
|
||||
selectedHookForDeletion?.method.schema,
|
||||
|
||||
@@ -12,8 +12,8 @@ import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useMaxConnectionsQuery } from 'data/database/max-connections-query'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import {
|
||||
@@ -46,7 +46,8 @@ const FormSchema = z.object({
|
||||
|
||||
export const PerformanceSettingsForm = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { hasAccess: hasAccessToPerformance, isLoading: isLoadingEntitlement } =
|
||||
useCheckEntitlements('auth.performance_settings')
|
||||
const { can: canReadConfig } = useAsyncCheckPermissions(
|
||||
PermissionAction.READ,
|
||||
'custom_config_gotrue'
|
||||
@@ -72,8 +73,7 @@ export const PerformanceSettingsForm = () => {
|
||||
})
|
||||
const maxConnectionLimit = maxConnData?.maxConnections ?? 60
|
||||
|
||||
const isProPlan = organization?.plan.id !== 'free'
|
||||
const promptProPlanUpgrade = IS_PLATFORM && !isProPlan
|
||||
const promptUpgrade = IS_PLATFORM && !isLoadingEntitlement && !hasAccessToPerformance
|
||||
|
||||
const { mutate: updateAuthConfig, isPending: isSaving } = useAuthConfigUpdateMutation()
|
||||
|
||||
@@ -101,7 +101,7 @@ export const PerformanceSettingsForm = () => {
|
||||
|
||||
const onSubmitRequestDurationForm = (values: any) => {
|
||||
if (!project?.ref) return console.error('Project ref is required')
|
||||
if (!isProPlan) return
|
||||
if (!hasAccessToPerformance) return
|
||||
|
||||
setIsUpdatingRequestDurationForm(true)
|
||||
|
||||
@@ -181,7 +181,7 @@ export const PerformanceSettingsForm = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoadingAuthConfig) {
|
||||
if (isLoadingAuthConfig || isLoadingEntitlement) {
|
||||
return (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<GenericSkeletonLoader />
|
||||
@@ -192,7 +192,7 @@ export const PerformanceSettingsForm = () => {
|
||||
return (
|
||||
<>
|
||||
<ScaffoldSection isFullWidth>
|
||||
{promptProPlanUpgrade && (
|
||||
{promptUpgrade && (
|
||||
<UpgradeToPro
|
||||
source="authPerformance"
|
||||
featureProposition="configure advanced Auth server settings"
|
||||
@@ -232,7 +232,7 @@ export const PerformanceSettingsForm = () => {
|
||||
min={5}
|
||||
max={30}
|
||||
{...field}
|
||||
disabled={!canUpdateConfig || promptProPlanUpgrade}
|
||||
disabled={!canUpdateConfig || promptUpgrade}
|
||||
/>
|
||||
</PrePostTab>
|
||||
</div>
|
||||
@@ -254,13 +254,13 @@ export const PerformanceSettingsForm = () => {
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type={promptProPlanUpgrade ? 'default' : 'primary'}
|
||||
type={promptUpgrade ? 'default' : 'primary'}
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!canUpdateConfig ||
|
||||
isUpdatingRequestDurationForm ||
|
||||
!requestDurationForm.formState.isDirty ||
|
||||
promptProPlanUpgrade
|
||||
promptUpgrade
|
||||
}
|
||||
loading={isUpdatingRequestDurationForm}
|
||||
>
|
||||
@@ -325,7 +325,7 @@ export const PerformanceSettingsForm = () => {
|
||||
>
|
||||
<SelectTrigger_Shadcn_
|
||||
size="small"
|
||||
disabled={!canUpdateConfig || promptProPlanUpgrade}
|
||||
disabled={!canUpdateConfig || promptUpgrade}
|
||||
>
|
||||
<SelectValue_Shadcn_>
|
||||
{field.value === 'percent' ? 'Percentage' : 'Absolute'}
|
||||
@@ -376,7 +376,7 @@ export const PerformanceSettingsForm = () => {
|
||||
? 80
|
||||
: Math.floor(maxConnectionLimit * 0.8)
|
||||
}
|
||||
disabled={!canUpdateConfig || promptProPlanUpgrade}
|
||||
disabled={!canUpdateConfig || promptUpgrade}
|
||||
/>
|
||||
</PrePostTab>
|
||||
</div>
|
||||
@@ -408,7 +408,7 @@ export const PerformanceSettingsForm = () => {
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type={promptProPlanUpgrade ? 'default' : 'primary'}
|
||||
type={promptUpgrade ? 'default' : 'primary'}
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!canUpdateConfig || isUpdatingDatabaseForm || !databaseForm.formState.isDirty
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { has, isEmpty, isEqual } from 'lodash'
|
||||
|
||||
import { ident } from '@supabase/pg-meta/src/pg-format'
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { generateSqlPolicy } from 'data/ai/sql-policy-mutation'
|
||||
import type { CreatePolicyBody } from 'data/database-policies/database-policy-create-mutation'
|
||||
import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
|
||||
import { has, isEmpty, isEqual } from 'lodash'
|
||||
|
||||
import {
|
||||
PolicyFormField,
|
||||
PolicyForReview,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
|
||||
export const createSQLPolicy = (
|
||||
policyFormFields: PolicyFormField,
|
||||
originalPolicyFormFields: PostgresPolicy
|
||||
originalPolicyFormFields?: PostgresPolicy
|
||||
) => {
|
||||
const { definition, check } = policyFormFields
|
||||
const formattedPolicyFormFields = {
|
||||
@@ -32,7 +32,7 @@ export const createSQLPolicy = (
|
||||
check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check,
|
||||
}
|
||||
|
||||
if (isEmpty(originalPolicyFormFields)) {
|
||||
if (!originalPolicyFormFields || isEmpty(originalPolicyFormFields)) {
|
||||
return createSQLStatementForCreatePolicy(formattedPolicyFormFields)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface PolicyEditorProps {
|
||||
onReviewPolicy: () => void
|
||||
}
|
||||
|
||||
const PolicyEditor = ({
|
||||
export const PolicyEditor = ({
|
||||
isNewPolicy = true,
|
||||
policyFormFields = {},
|
||||
onUpdatePolicyFormFields = () => {},
|
||||
@@ -75,5 +75,3 @@ const PolicyEditor = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PolicyEditor
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { isEmpty, noop } from 'lodash'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Modal } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
|
||||
import {
|
||||
PolicyFormField,
|
||||
PolicyForReview,
|
||||
PostgresPolicyCreatePayload,
|
||||
PostgresPolicyUpdatePayload,
|
||||
} from '../Policies.types'
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
createPayloadForUpdatePolicy,
|
||||
createSQLPolicy,
|
||||
} from '../Policies.utils'
|
||||
import PolicyEditor from '../PolicyEditor'
|
||||
import { PolicyEditor } from '../PolicyEditor'
|
||||
import { PolicyReview } from '../PolicyReview'
|
||||
import PolicySelection from '../PolicySelection'
|
||||
import PolicyTemplates from '../PolicyTemplates'
|
||||
@@ -30,7 +32,7 @@ interface PolicyEditorModalProps {
|
||||
visible?: boolean
|
||||
schema?: string
|
||||
table?: string
|
||||
selectedPolicyToEdit: any
|
||||
selectedPolicyToEdit?: PostgresPolicy
|
||||
showAssistantPreview?: boolean
|
||||
onSelectCancel: () => void
|
||||
onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise<boolean>
|
||||
@@ -38,11 +40,11 @@ interface PolicyEditorModalProps {
|
||||
onSaveSuccess: () => void
|
||||
}
|
||||
|
||||
const PolicyEditorModal = ({
|
||||
export const PolicyEditorModal = ({
|
||||
visible = false,
|
||||
schema = '',
|
||||
table = '',
|
||||
selectedPolicyToEdit = {},
|
||||
selectedPolicyToEdit,
|
||||
showAssistantPreview = false,
|
||||
onSelectCancel = noop,
|
||||
onCreatePolicy,
|
||||
@@ -71,10 +73,10 @@ const PolicyEditorModal = ({
|
||||
const [policyFormFields, setPolicyFormFields] = useState<PolicyFormField>(
|
||||
initializedPolicyFormFields
|
||||
)
|
||||
const [policyStatementForReview, setPolicyStatementForReview] = useState<any>('')
|
||||
const [policyStatementForReview, setPolicyStatementForReview] = useState<PolicyForReview>()
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
onSelectCancel()
|
||||
@@ -198,7 +200,7 @@ const PolicyEditorModal = ({
|
||||
onCancel={confirmOnClose}
|
||||
>
|
||||
<div>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
{view === POLICY_MODAL_VIEWS.SELECTION ? (
|
||||
<PolicySelection
|
||||
description="Write rules with PostgreSQL's policies to fit your unique business needs."
|
||||
@@ -221,7 +223,7 @@ const PolicyEditorModal = ({
|
||||
templatesNote="* References a specific column in the table"
|
||||
onUseTemplate={onUseTemplate}
|
||||
/>
|
||||
) : view === POLICY_MODAL_VIEWS.REVIEW ? (
|
||||
) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? (
|
||||
<PolicyReview
|
||||
policy={policyStatementForReview}
|
||||
onSelectBack={onViewEditor}
|
||||
@@ -232,20 +234,3 @@ const PolicyEditorModal = ({
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the editor? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
export default PolicyEditorModal
|
||||
@@ -12,12 +12,13 @@ import * as z from 'zod'
|
||||
import { useParams } from 'common'
|
||||
import { IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation'
|
||||
import { databasePoliciesKeys } from 'data/database-policies/keys'
|
||||
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
Tabs_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { LockedCreateQuerySection, LockedRenameQuerySection } from './LockedQuerySection'
|
||||
import { PolicyDetailsV2 } from './PolicyDetailsV2'
|
||||
import { checkIfPolicyHasChanged, generateCreatePolicyQuery } from './PolicyEditorPanel.utils'
|
||||
@@ -165,7 +165,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
return policyCreateUnsaved || policyUpdateUnsaved
|
||||
}, [command, name, roles, selectedPolicy])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: hasUnsavedChanges,
|
||||
onClose: onSelectCancel,
|
||||
})
|
||||
@@ -292,7 +292,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
<>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={showTools ? 'lg' : 'default'}
|
||||
@@ -573,24 +573,12 @@ export const PolicyEditorPanel = memo(function ({
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog
|
||||
{...modalProps}
|
||||
description="Are you sure you want to close the editor? Any unsaved changes on your policy and conversations with the Assistant will be lost."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
PolicyEditorPanel.displayName = 'PolicyEditorPanel'
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
@@ -64,5 +64,3 @@ export const PolicyReview = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PolicyReview
|
||||
@@ -11,8 +11,8 @@ import NoPermission from 'components/ui/NoPermission'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import {
|
||||
Button,
|
||||
@@ -83,9 +83,9 @@ export const SessionsAuthSettingsForm = () => {
|
||||
'custom_config_gotrue'
|
||||
)
|
||||
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const isProPlanAndUp = organization?.plan?.id !== 'free'
|
||||
const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp
|
||||
const { hasAccess: hasUserSessionsEntitlement, isLoading: isLoadingEntitlements } =
|
||||
useCheckEntitlements('auth.user_sessions')
|
||||
const promptProPlanUpgrade = IS_PLATFORM && !hasUserSessionsEntitlement
|
||||
|
||||
const refreshTokenForm = useForm<z.infer<typeof RefreshTokenSchema>>({
|
||||
resolver: zodResolver(RefreshTokenSchema),
|
||||
@@ -182,7 +182,7 @@ export const SessionsAuthSettingsForm = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || isLoadingEntitlements) {
|
||||
return (
|
||||
<PageSection>
|
||||
<PageSectionContent>
|
||||
@@ -304,7 +304,7 @@ export const SessionsAuthSettingsForm = () => {
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!canUpdateConfig || !isProPlanAndUp}
|
||||
disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
@@ -328,7 +328,7 @@ export const SessionsAuthSettingsForm = () => {
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
disabled={!canUpdateConfig || !isProPlanAndUp}
|
||||
disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
|
||||
/>
|
||||
</PrePostTab>
|
||||
</FormControl_Shadcn_>
|
||||
@@ -353,7 +353,7 @@ export const SessionsAuthSettingsForm = () => {
|
||||
type="number"
|
||||
{...field}
|
||||
className="flex-1"
|
||||
disabled={!canUpdateConfig || !isProPlanAndUp}
|
||||
disabled={!canUpdateConfig || !hasUserSessionsEntitlement}
|
||||
/>
|
||||
</PrePostTab>
|
||||
</FormControl_Shadcn_>
|
||||
|
||||