diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 1a6766deeab47..b88ab4709bbbe 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -148,6 +148,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "alert-dialog-close-only": { + name: "alert-dialog-close-only", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-close-only")), + source: "", + files: ["registry/default/example/alert-dialog-close-only.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "aspect-ratio-demo": { name: "aspect-ratio-demo", type: "components:example", @@ -159,6 +170,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "alert-dialog-destructive": { + name: "alert-dialog-destructive", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-destructive")), + source: "", + files: ["registry/default/example/alert-dialog-destructive.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "alert-dialog-warning": { + name: "alert-dialog-warning", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-warning")), + source: "", + files: ["registry/default/example/alert-dialog-warning.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "avatar-demo": { name: "avatar-demo", type: "components:example", @@ -1237,6 +1270,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "sheet-nonmodal": { + name: "sheet-nonmodal", + type: "components:example", + registryDependencies: ["sheet"], + component: React.lazy(() => import("@/registry/default/example/sheet-nonmodal")), + source: "", + files: ["registry/default/example/sheet-nonmodal.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "sheet-side": { name: "sheet-side", type: "components:example", @@ -1820,39 +1864,6 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "text-confirm-dialog-with-info-alert": { - name: "text-confirm-dialog-with-info-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-info-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-info-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-warning-alert": { - name: "text-confirm-dialog-with-warning-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-warning-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-warning-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-destructive-alert": { - name: "text-confirm-dialog-with-destructive-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-destructive-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-destructive-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, "text-confirm-dialog-with-size": { name: "text-confirm-dialog-with-size", type: "components:example", @@ -2458,46 +2469,13 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "modal-demo": { - name: "modal-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-demo")), - source: "", - files: ["registry/default/example/modal-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-aligned-footer": { - name: "modal-aligned-footer", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-aligned-footer")), - source: "", - files: ["registry/default/example/modal-aligned-footer.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-custom-footer": { - name: "modal-custom-footer", + "confirmation-modal-demo": { + name: "confirmation-modal-demo", type: "components:example", registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-custom-footer")), + component: React.lazy(() => import("@/registry/default/example/confirmation-modal-demo")), source: "", - files: ["registry/default/example/modal-custom-footer.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-hide-footer": { - name: "modal-hide-footer", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-hide-footer")), - source: "", - files: ["registry/default/example/modal-hide-footer.tsx"], + files: ["registry/default/example/confirmation-modal-demo.tsx"], category: "undefined", subcategory: "undefined", chunks: [] @@ -3005,7 +2983,7 @@ export const Index: Record = { source: "", files: ["registry/default/example/copy-error-messages.tsx"], category: "Getting Started", - subcategory: "Copywriting", + subcategory: "Copywriting", chunks: [] }, "copy-success-messages": { @@ -3060,7 +3038,7 @@ export const Index: Record = { source: "", files: ["registry/default/example/copy-confirmations.tsx"], category: "Getting Started", - subcategory: "Copywriting", + subcategory: "Copywriting", chunks: [] }, }, diff --git a/apps/design-system/app/(app)/page.tsx b/apps/design-system/app/(app)/page.tsx index 9540c812a469b..377ca6bdd0922 100644 --- a/apps/design-system/app/(app)/page.tsx +++ b/apps/design-system/app/(app)/page.tsx @@ -1,7 +1,7 @@ import { HomepageSvgHandler } from '@/components/homepage-svg-handler' -import Link from 'next/link' +import { Auth, Database, Realtime } from 'icons/src/icons' import { Paintbrush } from 'lucide-react' -import { Realtime, Database, Auth } from 'icons/src/icons' +import Link from 'next/link' export default function Home() { return ( @@ -15,75 +15,77 @@ export default function Home() {
- +
-
- +
+
-

Atom components

-

Building blocks of User interfaces

+

Colors

+

Custom color palette for Supabase

- + +
-
- +
+ + +
-

Fragment components

-

Components assembled from Atoms

+

Icons

+

Custom icons for Supabase

- + +
-
- +
+
-

UI Patterns components

-

- Components assembled from Atoms & Fragments -

+

Theming

+

Simple extensible theming system

- +
-
- +
+
-

Colors

-

Building blocks of User interfaces

+

UI patterns

+

+ Design guidelines for common interface patterns +

- +
-
- +
+
-

Theming

-

Simple extensible theming system

+

Fragment components

+

Components assembled from atoms

- +
-
- - - +
+
-

Icons

-

Custom icons for Supabase

+

Atom components

+

Building blocks of user interfaces

diff --git a/apps/design-system/components/component-preview.tsx b/apps/design-system/components/component-preview.tsx index 4dcfe8c56c0da..a16b7377c0c0e 100644 --- a/apps/design-system/components/component-preview.tsx +++ b/apps/design-system/components/component-preview.tsx @@ -1,8 +1,11 @@ 'use client' -import * as React from 'react' import { Index } from '@/__registry__' +import * as React from 'react' +import { useConfig } from '@/hooks/use-config' +import { styles } from '@/registry/styles' +import { ChevronRight, Expand } from 'lucide-react' import { Button, CollapsibleContent_Shadcn_, @@ -10,9 +13,6 @@ import { Collapsible_Shadcn_, cn, } from 'ui' -import { useConfig } from '@/hooks/use-config' -import { styles } from '@/registry/styles' -import { ChevronRight, Expand } from 'lucide-react' interface ComponentPreviewProps extends React.HTMLAttributes { name: string @@ -70,7 +70,7 @@ export function ComponentPreview({ return ( <>
## Installation @@ -85,3 +88,32 @@ import { ``` + +## Behavior + +Unlike a generic [Dialog](../components/dialog), an Alert Dialog cannot be dismissed by clicking outside the modal. The user must take an explicit action by confirming, cancelling, or pressing Escape. + +This enforced decision helps prevent accidental dismissal of critical warnings or destructive actions. + +## Guidelines + +- **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. +- **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. + +See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + +## Examples + +### Close only + + + +### Warning + + + +### Destructive + + diff --git a/apps/design-system/content/docs/components/dialog.mdx b/apps/design-system/content/docs/components/dialog.mdx index b8f9ae549d6cc..108d9852e0537 100644 --- a/apps/design-system/content/docs/components/dialog.mdx +++ b/apps/design-system/content/docs/components/dialog.mdx @@ -1,7 +1,6 @@ --- title: Dialog -description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. -featured: true +description: A general-purpose modal for non-critical flows, forms, and custom interactions. component: true links: doc: https://www.radix-ui.com/docs/primitives/components/dialog @@ -11,6 +10,20 @@ source: shadcn: true --- +Dialog is a flexible, general-purpose modal used for bespoke interactions such as forms, pickers, multi-step flows, or displaying non-urgent information. Unlike confirmation-focused dialogs, it is designed to be safely dismissible and does not force an explicit decision. + +Dialog can be closed by clicking outside the modal or pressing the Escape key, making it suitable for workflows where cancellation is expected and low-risk. + +Use Dialog when you need full control over layout, content, and behavior, and the interaction does not involve a critical or destructive action. + +For confirmations or warnings, try to use an existing component: + +- Use [Alert Dialog](../components/alert-dialog) for critical confirmations that require an explicit decision +- Use [Confirmation Modal](../fragments/confirmation-modal) when additional context is needed for a confirmation +- Use [Text Confirm Dialog](../fragments/text-confirm-dialog) for highly destructive actions that require typed intent + +See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + ## Usage @@ -31,23 +44,31 @@ import { Open - Are you absolutely sure? + Project settings - This action cannot be undone. This will permanently delete your account and remove your data - from our servers. + Update configuration options for this project. Changes can be discarded at any time. + {/* Custom content goes here */} ``` +## Guidelines + +- **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. +- **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. + ## Examples ### Custom close button -### Centered behaviour +### Centered behavior You can control whether the dialog is centered by passing `centered={false}` to the `DialogContent` component. @@ -69,30 +90,27 @@ You can control whether the dialog is centered by passing `centered={false}` to To activate the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or `Dropdown Menu` component in the `Dialog` component. For more information, refer to the linked issue [here](https://github.com/radix-ui/primitives/issues/1836). -```tsx {14-25} +```tsx {7-11, 14-23} - Dialog not centered + Show Menu Open Download - Delete + Show Dialog - Are you absolutely sure? - - This action cannot be undone. Are you sure you want to permanently delete this file from our - servers? - + Edit profile + Make changes to your profile here. - + diff --git a/apps/design-system/content/docs/components/atom-components.mdx b/apps/design-system/content/docs/components/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/components/atom-components.mdx rename to apps/design-system/content/docs/components/introduction.mdx diff --git a/apps/design-system/content/docs/components/sheet.mdx b/apps/design-system/content/docs/components/sheet.mdx index 96220e63eb577..75edbb5386010 100644 --- a/apps/design-system/content/docs/components/sheet.mdx +++ b/apps/design-system/content/docs/components/sheet.mdx @@ -12,6 +12,22 @@ source: +1. **Use for side panels** + + - Forms with multiple fields + - Settings panels + - Detailed editors + +2. **Consider screen size** + + - Sheets work well on desktop + - On mobile, consider full-screen or bottom sheet variants + +3. **Structure content clearly** + - Use `SheetHeader` and `SheetTitle` for context + - Use `SheetSection` to group related fields + - Use `SheetFooter` for actions + ## Installation @@ -80,11 +96,21 @@ import { ## Examples -### Side +### Nonmodal -Use the `side` property to `` to indicate the edge of the screen where the component will appear. The values can be `top`, `right`, `bottom` or `left`. +This sheet is nonmodal, meaning it does not block the underlying content. It’s useful when +you want to display content that complements the main content of the screen. - + + +To have the underlying content resize to fit the sheet (so nothing is overlapping) use the Sidebar component +or build a custom panel. You can refer to the following Studio components for guidance: + +- `AIAssistant` +- `EditorPanel` +- `AdvisorPanel` + +See [`LayoutSidebarProvider`](https://github.com/supabase/supabase/blob/master/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx) for more. ### Size @@ -104,3 +130,11 @@ You can adjust the size of the sheet using CSS classes: ``` + +### Side + +Use the `side` property to `` to indicate the edge of the screen where the component will appear. The values can be `top`, `right`, `bottom` or `left`. + + + +That said, stick to the default `right` unless you have a strong reason not to. diff --git a/apps/design-system/content/docs/fragments/confirmation-modal.mdx b/apps/design-system/content/docs/fragments/confirmation-modal.mdx new file mode 100644 index 0000000000000..dfe727fa17115 --- /dev/null +++ b/apps/design-system/content/docs/fragments/confirmation-modal.mdx @@ -0,0 +1,71 @@ +--- +title: Confirmation Modal +description: A modal dialog for confirmations that require additional context or simple interaction. +component: true +--- + +Confirmation Modal is a convenience wrapper for confirmation flows that are more complex than a single paragraph but do not warrant a full custom dialog. It is built on top of [Dialog](../components/dialog) and provides a prop-based API for consistent confirmation patterns. + +Use Confirmation Modal when the user needs extra context to make a decision, such as explanatory copy, callouts, or small form elements, and the action is not so destructive that it requires typed confirmation. + +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. + + + +## Usage + +```tsx +'use client' + +import { useState } from 'react' +import { Button } from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +``` + +```tsx +export default function ConfirmationModalDemo() { + const [visible, setVisible] = useState(false) + + return ( + <> + + + { + setVisible(false) + }} + onCancel={() => { + setVisible(false) + }} + > + This will resume the project and restart any paused processes. + + + ) +} +``` + +## Guidelines + +- **Use for moderate complexity:** Suitable when the confirmation requires more than a single sentence but does not need typed intent. +- **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. + +## Props + +- `visible`: Controls open state +- `title`: Dialog title +- `description`: Optional description +- `variant`: 'default' | 'destructive' | 'warning' +- `loading`: Loading state +- `onConfirm`: Confirm handler +- `onCancel`: Cancel handler +- `alert`: Optional callout (see [Admonition](../fragments/admonition)) +- `children`: Additional content diff --git a/apps/design-system/content/docs/fragments/empty-state-presentational.mdx b/apps/design-system/content/docs/fragments/empty-state-presentational.mdx index e0936a1610471..361be6250cbfc 100644 --- a/apps/design-system/content/docs/fragments/empty-state-presentational.mdx +++ b/apps/design-system/content/docs/fragments/empty-state-presentational.mdx @@ -1,5 +1,5 @@ --- -title: EmptyStatePresentational +title: Empty State Presentational description: An empty state for encouraging action. component: true fragment: true @@ -17,7 +17,7 @@ All text should be written using active language. The title should prompt the us ### Icon -Supports both Lucide icons and [custom icons](../icons) via the `icons` package. If neither are passed, EmptyStatePresentational falls back to Lucide’s `SquarePlus`. +Supports both Lucide icons and [custom icons](../icons) via the `icons` package. If neither are passed, Empty State Presentational falls back to Lucide’s `SquarePlus`. @@ -25,10 +25,10 @@ See also [Empty States](../ui-patterns/empty-states). ## Examples -It’s okay to repeat buttons inside of EmptyStatePresentational that are also available outside of it. The alternative is to conditionally determine button placement whilst polling for list length (to determine whether to show an empty state or not). This is problematic for two reasons: +It’s okay to repeat buttons inside of Empty State Presentational that are also available outside of it. The alternative is to conditionally determine button placement whilst polling for list length (to determine whether to show an empty state or not). This is problematic for two reasons: 1. Rendering after client-side polling often leads to confusing layout shift. This layout shift becomes exacerbated when buttons are stacked against other objects. -2. Consistent entry points outside of EmptyStatePresentational also teach a pattern that will continue to exist post initial object creation. +2. Consistent entry points outside of Empty State Presentational also teach a pattern that will continue to exist post initial object creation. When repeating buttons, set the `type` to `default` so the original `primary`, button remains the only `primary` action on display. diff --git a/apps/design-system/content/docs/fragments/fragment-components.mdx b/apps/design-system/content/docs/fragments/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/fragments/fragment-components.mdx rename to apps/design-system/content/docs/fragments/introduction.mdx diff --git a/apps/design-system/content/docs/fragments/modal.mdx b/apps/design-system/content/docs/fragments/modal.mdx deleted file mode 100644 index 2bbd59837cf96..0000000000000 --- a/apps/design-system/content/docs/fragments/modal.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Modal -description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. -fragment: true -links: - doc: https://www.radix-ui.com/docs/primitives/components/dialog - api: https://www.radix-ui.com/docs/primitives/components/dialog#api-reference ---- - - - -## Examples - -### Aligned footer - - - -### Hide footer - - - -### Custom footer - - diff --git a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx index f9127a0ebe756..4b812ead978fc 100644 --- a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx +++ b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx @@ -1,57 +1,73 @@ --- title: Text Confirm Dialog -description: A modal dialog that interrupts the user with important content and expects a response. +description: A modal dialog that adds a deliberate confirmation step for highly destructive actions. component: true --- - +Text Confirm Dialog adds a deliberate “speed bump” before a highly destructive action by requiring the user to type an exact confirmation string before the confirm action is enabled. It wraps the Shadcn [Dialog](../components/dialog) component and is intended for actions that must not be triggered accidentally. -## Examples +Use Text Confirm Dialog for irreversible operations such as deleting buckets, projects, or other critical resources where an explicit signal of user intent is required beyond a button click. + +For non-destructive or less critical confirmations, use [Alert Dialog](../components/alert-dialog) or [Confirmation Modal](../fragments/confirmation-modal) instead. See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + + + +## Usage -### With Info Alert +```tsx +'use client' - +import { useState } from 'react' +import { Button } from 'ui' +import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' +``` -### With warning Alert +```tsx +export default function TextConfirmDialogDemo() { + const [visible, setVisible] = useState(false) + const bucketName = 'profile-pictures' - + return ( + <> + -### With destructive Alert + setVisible(false)} + onCancel={() => setVisible(false)} + > + {/* Optional body content */} + + + ) +} +``` - +## Props + +- `confirmString`: The exact string the user must type to enable the confirm action +- `confirmPlaceholder`: Placeholder text shown in the confirmation input +- `variant`: Visual intent of the dialog (`default`, `destructive`, or `warning`) +- Other standard modal props inherited from the underlying [Dialog](../components/dialog) component + +## Examples -### With cancel button +### Cancel button - + -### With children +### Children - + -### With size +### Size - + diff --git a/apps/design-system/content/docs/ui-patterns/empty-states.mdx b/apps/design-system/content/docs/ui-patterns/empty-states.mdx index 851fda3068e39..f4dc21628e56d 100644 --- a/apps/design-system/content/docs/ui-patterns/empty-states.mdx +++ b/apps/design-system/content/docs/ui-patterns/empty-states.mdx @@ -3,28 +3,30 @@ title: Empty states description: Convey the absence of data and provide clear instruction for what to do about it. --- -Empty states convey the fact that there is nothing to list, perform, or display on the current page. **Ideally**, they also provide a clear action for the user to take. +Empty states convey the fact that there is nothing to list, perform, or display on the current page. Ideally, they also provide a clear action for the user to take. -## No data +## Best practices + +### No data There are two ways an empty state may be displayed in cases where there is no data: - **Initial state**: no data to begin with - **Zero results**: no data after a search or filter -### Initial state +#### Initial state Perhaps the user has not yet created any data. The presentation of this empty state depends on the context of the list and the type of data it contains. Be mindful of the journey to rendering an empty state and any possible layout shift along the way. -#### Presentational +##### Presentational -The user may be learning about a feature for the first time, and could benefit from lightweight feature education or onboarding. Use the dedicated [EmptyStatePresentational](../fragments/empty-state-presentational) component in this case, putting emphasis on an action the user can take. +The user may be learning about a feature for the first time, and could benefit from lightweight feature education or onboarding. Use the dedicated [Empty State Presentational](../fragments/empty-state-presentational) component in this case, putting emphasis on an action the user can take. Remember to use active language in presentational empty states. For example: “Create a vector bucket” instead of “No vector buckets found”. The latter is more appropriate in table-based presentations, as described below. -#### Informational +##### Informational Or perhaps the list type is data-heavy or does not benefit from additional information. In these cases, the empty state should provide show the initial state in the same presentation as the list when there is data, much like the [zero results](#zero-results) scenario. @@ -32,11 +34,11 @@ Or perhaps the list type is data-heavy or does not benefit from additional infor Keep in mind that empty states will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition. -### Zero results +#### Zero results Data-heavy presentations without results should have an empty state that broadly matches the state when there is data. This makes the transition between the two states more seamless. -#### Table +##### Table A [Table](../components/table) instance with zero results should display a single row. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data. @@ -47,7 +49,7 @@ Studio contains two pre-built components to handle these cases consistently: - No Filter Results - No Search Results -#### Data Grid +##### Data Grid [Data Grid](../ui-patterns/tables#data-grid) and [Data Table](../ui-patterns/tables#data-table) component patterns typically span the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration. @@ -55,7 +57,7 @@ Studio contains two pre-built components to handle these cases consistently: Other Data Grid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues). -## Missing route +### Missing route Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in [Storage](https://supabase.com/dashboard/project/_/storage) or a non-existent table in the [Table Editor](https://supabase.com/dashboard/project/_/editor). In these cases, follow the pattern of a centered [Admonition](../fragments/admonition) as shown below. @@ -63,6 +65,6 @@ Users may accidentally navigate to a non-existent dynamic route, such as a non-e ## Components -For presentational empty states (initial states with value propositions and actions), use the [EmptyStatePresentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons. +For presentational empty states (initial states with value propositions and actions), use the [Empty State Presentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons. For other empty state scenarios (zero results, missing routes, etc), custom components may still be appropriate as the context and needs for each placement can differ significantly. diff --git a/apps/design-system/content/docs/ui-patterns/ui-patterns.mdx b/apps/design-system/content/docs/ui-patterns/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/ui-patterns/ui-patterns.mdx rename to apps/design-system/content/docs/ui-patterns/introduction.mdx diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx new file mode 100644 index 0000000000000..3da04913adf5e --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -0,0 +1,76 @@ +--- +title: Modality +description: Present ephemeral information and demand action. +--- + +Modal elements interrupt the user’s current task to ask for input, a decision, or focused attention. They appear at the top of the visual stack and (by default) render everything beneath them inactive. + +Given their highly interruptive nature, modal elements should be used sparingly. Common use cases include: + +- Requiring confirmation from the user +- Requiring an ephemeral form submission from the user before an action can be completed +- Alerting or slowing the user down before a destructive action + +We have two main ways of handling modality: + +- [Dialogs](#dialogs) +- [Sheets](#sheets) + +As a general rule: use dialogs for short, focused tasks and use sheets for longer forms or more detailed views. + +## Dialogs + +Dialogs are centered overlays used for short, focused tasks. All dialogs should follow these best practices: + +- **Reiterative:** Dialog header and confirmation button text and should match the action and flow on from the entry point. +- **Simple:** No layered elements like subtitles or admonitions unless necessary. Put all the focus on the actions to get out of the dialog. +- **Accessible:** Always provide clear labels and descriptions via semantic HTML and the correct ARIA attributes. Ensure keyboard navigation works correctly. + +### Components + +There are quite a few dialog components, each suited to a different task or context: + +- [Alert Dialog](../components/alert-dialog) contains a single, short paragraph and an explicit action. +- [Text Confirm Dialog](../fragments/text-confirm-dialog) requires a textual response before the action is enabled. +- [Confirmation Modal](../fragments/confirmation-modal) provides more flexible dialog body contents. +- [Dialog](../components/dialog) is a generalized component for bespoke purposes. + +#### Alert Dialog + +[Alert Dialog](../components/alert-dialog) is used to confirm or acknowledge a critical action with a single, short paragraph and a clear decision. + + + +#### Text Confirm Dialog + +[Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. + + + +#### Confirmation Modal + +[Confirmation Modal](../fragments/confirmation-modal) is a convenience wrapper for less-critical confirmations that require more than a single paragraph, such as additional context, callouts, or simple form elements. + + + +#### Dialog + +[Dialog](../components/dialog) is a general-purpose modal for bespoke flows such as forms, pickers, or non-critical interactions where dismissal is acceptable. + + + +## Sheets + +Sheets are dialogs presented as side panels. Use them for content that is larger than a few fields, or when a centered dialog would feel cramped. + +- **Use for**: multi-field forms, editors, settings panels, and detailed views. +- **Prefer the default**: sheets slide in from the right unless you have a strong reason to use another side. +- **Group content**: use header/sections/footer so the user can scan and act quickly. + +### Components + +#### Sheet + +[Sheet](../components/sheet) is modal by default, blocking interaction with the underlying page. + + diff --git a/apps/design-system/registry/default/example/alert-dialog-close-only.tsx b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx new file mode 100644 index 0000000000000..204d3e3b97051 --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx @@ -0,0 +1,33 @@ +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogCloseOnly() { + return ( + + + + + + + Application submitted + + Thank you for your submission! Please check your email for a confirmation link to + complete your application. + + + + Close + + + + ) +} diff --git a/apps/design-system/registry/default/example/alert-dialog-demo.tsx b/apps/design-system/registry/default/example/alert-dialog-demo.tsx index 38f358327a40c..447be8975eb1f 100644 --- a/apps/design-system/registry/default/example/alert-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-demo.tsx @@ -8,26 +8,27 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, + Button, } from 'ui' -import { Button } from 'ui' export default function AlertDialogDemo() { return ( - + - Are you absolutely sure? + Create new API keys - This action cannot be undone. This will permanently delete your account and remove your - data from our servers. + This will create a default publishable key and a default secret key both named{' '} + default. These keys are required to connect + your application to your Supabase project. Cancel - Continue + Create keys diff --git a/apps/design-system/registry/default/example/alert-dialog-destructive.tsx b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx new file mode 100644 index 0000000000000..6edea2532a17a --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx @@ -0,0 +1,37 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogDestructive() { + return ( + + + + + + + + Delete hello-world + + + This action cannot be undone. Ensure that you have a backup in case you want to restore + this edge function. + + + + Cancel + Delete + + + + ) +} diff --git a/apps/design-system/registry/default/example/alert-dialog-warning.tsx b/apps/design-system/registry/default/example/alert-dialog-warning.tsx new file mode 100644 index 0000000000000..8b95feaa82aa8 --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-warning.tsx @@ -0,0 +1,35 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogWarning() { + return ( + + + + + + + Update branch + + This branch has 3 modified edge functions that will be overwritten when updating with + the latest functions from the production branch. This action cannot be undone. + + + + Cancel + Update + + + + ) +} diff --git a/apps/design-system/registry/default/example/confirmation-modal-demo.tsx b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx new file mode 100644 index 0000000000000..dc1d91e32d1c4 --- /dev/null +++ b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { + Button, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +export default function ConfirmationModalDemo() { + const [visible, setVisible] = useState(false) + const form = useForm({ + defaultValues: { + postgresVersion: '17.6.1.054', + }, + }) + + return ( + <> + + setVisible(false)} + onConfirm={() => {}} + loading={false} + confirmLabel="Resume" + confirmLabelLoading="Resuming" + cancelLabel="Cancel" + > + {/* Dialog contents */} +
+ {/* Text content */} +

+ Your project’s data will be restored to when it was initially paused. +

+ {/* Dropdown for Postgres version */} +
+ + ( + + + + + + + + 17.6.1.054 + 17.6.1.055 + + + + + )} + /> + +
+
+
+ + ) +} diff --git a/apps/design-system/registry/default/example/dialog-centered-off.tsx b/apps/design-system/registry/default/example/dialog-centered-off.tsx index 54713321a2565..fd32ad1d7a424 100644 --- a/apps/design-system/registry/default/example/dialog-centered-off.tsx +++ b/apps/design-system/registry/default/example/dialog-centered-off.tsx @@ -1,29 +1,31 @@ -import { Button, DialogSection, DialogSectionSeparator } from 'ui' import { + Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, + DialogSection, + DialogSectionSeparator, DialogTitle, DialogTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DialogDemo() { return ( - + - + This dialog is not centered. This dialog is not centered. - +
Name @@ -37,7 +39,7 @@ export default function DialogDemo() {
- +
diff --git a/apps/design-system/registry/default/example/dialog-close-button.tsx b/apps/design-system/registry/default/example/dialog-close-button.tsx index 4c45847ef7b7e..1194933899b79 100644 --- a/apps/design-system/registry/default/example/dialog-close-button.tsx +++ b/apps/design-system/registry/default/example/dialog-close-button.tsx @@ -1,18 +1,20 @@ import { Copy } from 'lucide-react' -import { Button, DialogSection, DialogSectionSeparator } from 'ui' import { + Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, + DialogSection, + DialogSectionSeparator, DialogTitle, DialogTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DialogCloseButton() { return ( @@ -21,12 +23,12 @@ export default function DialogCloseButton() { - + Share link Anyone who has this link will be able to view this. - +
@@ -44,7 +46,7 @@ export default function DialogCloseButton() {
- + + - + Edit profile - - Make changes to your profile here. Click save when you're done. - + Make changes to your profile here. - +
- - Name - - + Name +
- - Username - - + Username +
- - + +
diff --git a/apps/design-system/registry/default/example/drawer-dialog.tsx b/apps/design-system/registry/default/example/drawer-dialog.tsx index a89ce2117b812..599cd5bd8b040 100644 --- a/apps/design-system/registry/default/example/drawer-dialog.tsx +++ b/apps/design-system/registry/default/example/drawer-dialog.tsx @@ -2,18 +2,16 @@ import * as React from 'react' -import { cn } from '@/lib/utils' import { useMediaQuery } from '@/hooks/use-media-query' -import { Button } from 'ui' +import { cn } from '@/lib/utils' import { + Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, -} from 'ui' -import { Drawer, DrawerClose, DrawerContent, @@ -22,9 +20,9 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DrawerDialogDemo() { const [open, setOpen] = React.useState(false) @@ -34,7 +32,7 @@ export default function DrawerDialogDemo() { return ( - + @@ -52,7 +50,7 @@ export default function DrawerDialogDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/modal-demo.tsx b/apps/design-system/registry/default/example/modal-demo.tsx deleted file mode 100644 index f11c441568be1..0000000000000 --- a/apps/design-system/registry/default/example/modal-demo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Link2 } from 'lucide-react' -import { useState } from 'react' -import { Button, Modal } from 'ui' - -export default function ModalDemo() { - const [visible, setVisible] = useState(false) - - return ( - <> - - setVisible(!visible)} - onConfirm={() => setVisible(!visible)} - title="This is the title of the modal" - description="And i am the description" - size="medium" - hideClose={false} - header={ -
-
- -
-
-

This is the title

- This is the title -
-
- } - > - -

- Modal content is inserted here, if you need to insert anything into the Modal you can do - so via `children`. -

-
-
- - ) -} diff --git a/apps/design-system/registry/default/example/sheet-demo.tsx b/apps/design-system/registry/default/example/sheet-demo.tsx index 8dd0044aadbdc..174eb0eb253fd 100644 --- a/apps/design-system/registry/default/example/sheet-demo.tsx +++ b/apps/design-system/registry/default/example/sheet-demo.tsx @@ -1,11 +1,14 @@ -import { Button, Input_Shadcn_, Label_Shadcn_ } from 'ui' import { + Button, + Input_Shadcn_, + Label_Shadcn_, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, + SheetSection, SheetTitle, SheetTrigger, } from 'ui' @@ -14,34 +17,34 @@ export default function SheetDemo() { return ( - + - + Edit profile - - Make changes to your profile here. Click save when you re done. - + Make changes to your profile here. -
-
- - Name - - -
-
- - Username - - -
+
+ +
+
+ + Name + + +
+
+ + Username + + +
+
+
- + diff --git a/apps/design-system/registry/default/example/sheet-nonmodal.tsx b/apps/design-system/registry/default/example/sheet-nonmodal.tsx new file mode 100644 index 0000000000000..9129bc48dddca --- /dev/null +++ b/apps/design-system/registry/default/example/sheet-nonmodal.tsx @@ -0,0 +1,38 @@ +import { + Button, + Sheet, + SheetClose, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, + SheetTrigger, +} from 'ui' + +export default function SheetNonmodal() { + return ( + + + + + + + Log details + +
+ +

+ This sheet does not block the underlying content, but it does overlap it. +

+
+
+ + + + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/sheet-side.tsx b/apps/design-system/registry/default/example/sheet-side.tsx index edbd6b1f4f2b5..bf2307665d513 100644 --- a/apps/design-system/registry/default/example/sheet-side.tsx +++ b/apps/design-system/registry/default/example/sheet-side.tsx @@ -1,9 +1,9 @@ 'use client' -import { Button } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' import { + Button, + Input_Shadcn_, + Label_Shadcn_, Sheet, SheetClose, SheetContent, @@ -24,7 +24,7 @@ export default function SheetSide() { {SHEET_SIDES.map((side) => ( - + diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx index 9303fb27aaaa6..09292b5bfa094 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx @@ -1,46 +1,36 @@ 'use client' import { useState } from 'react' -import { toast } from 'sonner' - import { Button } from 'ui' import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' -const TextConfirmModalPrimary = () => { +export default function TextConfirmDialogDemo() { const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } + const bucketName = 'profile-pictures' return ( <> - + + variant="destructive" + title="Delete bucket" + confirmPlaceholder={bucketName} + confirmString={bucketName} + confirmLabel="Delete bucket" + loading={false} + onConfirm={() => setVisible(false)} + onCancel={() => setVisible(false)} + > +

+ Your bucket {bucketName} and all of + its contents will be permanently deleted. This action cannot be undone. +

+
) } - -export default TextConfirmModalPrimary diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx index 99a2328ef19ed..e629f3e7901c1 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx @@ -25,8 +25,8 @@ const TextConfirmModalWithCancelButton = () => { return ( <> - { return ( <> - { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithDestructiveAlert diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx deleted file mode 100644 index fef97c1db9cbe..0000000000000 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import { useState } from 'react' -import { toast } from 'sonner' - -import { Button } from 'ui' -import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' - -const TextConfirmModalWithInfoAlert = () => { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithInfoAlert diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx index a42f783a102c9..e0772b0282171 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx @@ -25,8 +25,8 @@ const TextConfirmModalWithSize = () => { return ( <> - { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { - description: 'Friday, February 10, 2023 at 5:57 PM', - }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithWarningAlert diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 6824a0459538f..699d249b6827f 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -43,6 +43,24 @@ export const examples: Registry = [ registryDependencies: ['alert-dialog', 'button'], files: ['example/alert-dialog-demo.tsx'], }, + { + name: 'alert-dialog-close-only', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-close-only.tsx'], + }, + { + name: 'alert-dialog-destructive', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-destructive.tsx'], + }, + { + name: 'alert-dialog-warning', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-warning.tsx'], + }, { name: 'aspect-ratio-demo', type: 'components:example', @@ -731,6 +749,12 @@ export const examples: Registry = [ registryDependencies: ['sheet'], files: ['example/sheet-demo.tsx'], }, + { + name: 'sheet-nonmodal', + type: 'components:example', + registryDependencies: ['sheet'], + files: ['example/sheet-nonmodal.tsx'], + }, { name: 'sheet-side', type: 'components:example', @@ -1039,21 +1063,6 @@ export const examples: Registry = [ type: 'components:example', files: ['example/text-confirm-dialog-demo.tsx'], }, - { - name: 'text-confirm-dialog-with-info-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-info-alert.tsx'], - }, - { - name: 'text-confirm-dialog-with-warning-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-warning-alert.tsx'], - }, - { - name: 'text-confirm-dialog-with-destructive-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-destructive-alert.tsx'], - }, { name: 'text-confirm-dialog-with-size', type: 'components:example', @@ -1360,24 +1369,9 @@ export const examples: Registry = [ files: ['example/tree-view-multi-select.tsx'], }, { - name: 'modal-demo', - type: 'components:example', - files: ['example/modal-demo.tsx'], - }, - { - name: 'modal-aligned-footer', - type: 'components:example', - files: ['example/modal-aligned-footer.tsx'], - }, - { - name: 'modal-custom-footer', - type: 'components:example', - files: ['example/modal-custom-footer.tsx'], - }, - { - name: 'modal-hide-footer', + name: 'confirmation-modal-demo', type: 'components:example', - files: ['example/modal-hide-footer.tsx'], + files: ['example/confirmation-modal-demo.tsx'], }, { name: 'assistant-chat-demo', diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index cd7cfa7e8f307..3d23504a92fa6 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,5 +1,6 @@ import '@code-hike/mdx/styles' import 'config/code-hike.scss' +import 'ui-patterns/ShimmeringLoader/index.css' import '../styles/main.scss' import '../styles/new-docs.scss' import '../styles/prism-okaidia.scss' diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 9fc45bcbbde8e..02c547ae4ee9c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -925,18 +925,6 @@ export const auth: NavMenuConstant = { }, ], }, - { - name: 'Auth UI', - url: undefined, - enabled: allAuthProvidersEnabled, - items: [ - { name: 'Auth UI (Deprecated)', url: '/guides/auth/auth-helpers/auth-ui' }, - { - name: 'Flutter Auth UI', - url: '/guides/auth/auth-helpers/flutter-auth-ui' as `/${string}`, - }, - ], - }, ], } diff --git a/apps/docs/content/_partials/auth_helpers.mdx b/apps/docs/content/_partials/auth_helpers.mdx deleted file mode 100644 index ccbf1888699a0..0000000000000 --- a/apps/docs/content/_partials/auth_helpers.mdx +++ /dev/null @@ -1,5 +0,0 @@ - - -The Auth helpers package is deprecated. Use the new `@supabase/ssr` package for Server Side Authentication. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Read out the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - - diff --git a/apps/docs/content/_partials/social_provider_setup.mdx b/apps/docs/content/_partials/social_provider_setup.mdx index 60b4a3dca689e..74b1d3423ae35 100644 --- a/apps/docs/content/_partials/social_provider_setup.mdx +++ b/apps/docs/content/_partials/social_provider_setup.mdx @@ -7,6 +7,6 @@ The next step requires a callback URL, which looks like this: `https:// -For testing OAuth locally with the Supabase CLI see the [local development docs](/docs/guides/cli/local-development#use-auth-locally). +For testing OAuth locally with the Supabase CLI see the [local development docs](/docs/guides/local-development). diff --git a/apps/docs/content/guides/auth/auth-helpers.mdx b/apps/docs/content/guides/auth/auth-helpers.mdx deleted file mode 100644 index 9c1d1653e9a04..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: 'index' -title: 'Auth Helpers' -description: 'Server-Side Auth guides and utilities for working with Supabase.' -sidebar_label: 'Overview' ---- - -<$Partial -path="auth_helpers.mdx" -/> - -Working with server-side frameworks is slightly different to client-side frameworks. In this section we cover the various ways of handling server-side authentication and demonstrate how to use the Supabase helper-libraries to make the process more seamless. - -
-
- {/* Next.js */} -
- -
- {/* SvelteKit */} -
- -
- {/* Remix */} -
- -
-
-
- -## Status - -The Auth Helpers are `deprecated`. Use the new `@supabase/ssr` package for Server Side Authentication. Use the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - -## Additional links - -- [Source code](https://github.com/supabase/auth-helpers) -- [Known bugs and issues](https://github.com/supabase/auth-helpers/issues) diff --git a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx b/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx deleted file mode 100644 index 2debcbdc92cbc..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx +++ /dev/null @@ -1,492 +0,0 @@ ---- -id: 'auth-ui' -title: 'Auth UI' -description: 'A prebuilt, customizable React component for authenticating users.' -sitemapPriority: 0.3 ---- - - - -As of 7th Feb 2024, [this repository](https://github.com/supabase-community/auth-ui) is no longer maintained by the Supabase Team. At the moment, the team does not have capacity to give the expected level of care to this repository. We may revisit Auth UI in the future but regrettably have to leave it on hold for now as we focus on other priorities such as improving the Server-Side Rendering (SSR) package and advanced Auth primitives. - -As an alternative you can use the [Supabase UI Library](/ui) which has auth ready blocks to use in your projects. - - - -Auth UI is a pre-built React component for authenticating users. -It supports custom themes and extensible styles to match your brand and aesthetic. - - - -## Set up Auth UI - -Install the latest version of [supabase-js](/docs/reference/javascript) and the Auth UI package: - -```bash -npm install @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shared -``` - -### Import the Auth component - -Pass `supabaseClient` from `@supabase/supabase-js` as a prop to the component. - -```js /src/index.js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const App = () => -``` - -This renders the Auth component without any styling. -We recommend using one of the predefined themes to style the UI. -Import the theme you want to use and pass it to the `appearance.theme` prop. - -```js -import { Auth } from '@supabase/auth-ui-react' -import { - // Import predefined theme - ThemeSupa, -} from '@supabase/auth-ui-shared' - -const supabase = createClient( - '', - '' -) - -const App = () => ( - -) -``` - -### Social providers - -The Auth component also supports login with [official social providers](../../auth#providers). - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' - -const supabase = createClient('', '') - -const App = () => ( - -) -``` - -### Options - -Options are available via `queryParams`: - -```jsx - -``` - -### Provider scopes - -Provider Scopes can be requested through `providerScope`; - -```jsx - -``` - -### Supported views - -The Auth component is currently shipped with the following views: - -- [Email Login](../auth-email) -- [Magic Link login](../auth-magic-link) -- [Social Login](../social-login) -- Update password -- Forgotten password - -We are planning on adding more views in the future. Follow along on that [repo](https://github.com/supabase/auth-ui). - -## Customization - -There are several ways to customize Auth UI: - -- Use one of the [predefined themes](#predefined-themes) that comes with Auth UI -- Extend a theme by [overriding the variable tokens](#override-themes) in a theme -- [Create your own theme](#create-theme) -- [Use your own CSS classes](#custom-css-classes) -- [Use inline styles](#custom-inline-styles) -- [Use your own labels](#custom-labels) - -### Predefined themes - -Auth UI comes with several themes to customize the appearance. Each predefined theme comes with at least two variations, a `default` variation, and a `dark` variation. You can switch between these themes using the `theme` prop. Import the theme you want to use and pass it to the `appearance.theme` prop. - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' - -const supabase = createClient( - '', - '' -) - -const App = () => ( - -) -``` - - - -Currently there is only one predefined theme available, but we plan to add more. - - - -### Switch theme variations - -Auth UI comes with two theme variations: `default` and `dark`. You can switch between these themes with the `theme` prop. - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' - -const supabase = createClient( - '', - '' -) - -const App = () => ( - -) -``` - -If you don't pass a value to `theme` it uses the `"default"` theme. You can pass `"dark"` to the theme prop to switch to the `dark` theme. If your theme has other variations, use the name of the variation in this prop. - -### Override themes - -Auth UI themes can be overridden using variable tokens. See the [list of variable tokens](https://github.com/supabase/auth-ui/blob/main/packages/shared/src/theming/Themes.ts). - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' - -const supabase = createClient('', '') - -const App = () => ( - -) -``` - -If you created your own theme, you may not need to override any of them. - -### Create your own theme [#create-theme] - -You can create your own theme by following the same structure within a `appearance.theme` property. -See the list of [tokens within a theme](https://github.com/supabase/auth-ui/blob/main/packages/shared/src/theming/Themes.ts). - -```js /src/index.js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const customTheme = { - default: { - colors: { - brand: 'hsl(153 60.0% 53.0%)', - brandAccent: 'hsl(154 54.8% 45.1%)', - brandButtonText: 'white', - // .. - }, - }, - dark: { - colors: { - brandButtonText: 'white', - defaultButtonBackground: '#2e2e2e', - defaultButtonBackgroundHover: '#3e3e3e', - //.. - }, - }, - // You can also add more theme variations with different names. - evenDarker: { - colors: { - brandButtonText: 'white', - defaultButtonBackground: '#1e1e1e', - defaultButtonBackgroundHover: '#2e2e2e', - //.. - }, - }, -} - -const App = () => ( - -) -``` - -You can switch between different variations of your theme with the ["theme" prop](#switch-theme-variations). - -### Custom CSS classes [#custom-css-classes] - -You can use custom CSS classes for the following elements: -`"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`. - -```js /src/index.js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const App = () => ( - -) -``` - -### Custom inline CSS [#custom-inline-styles] - -You can use custom CSS inline styles for the following elements: -`"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`. - -```js /src/index.js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const App = () => ( - -) -``` - -### Custom labels [#custom-labels] - -You can use custom labels with `localization.variables` like so: - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const App = () => ( - -) -``` - -A full list of the available variables is below: - - - - -| Label Tag | Default Label | -| ---------------------------- | ------------------------------------------ | -| `email_label` | Email address | -| `password_label` | Create a Password | -| `email_input_placeholder` | Your email address | -| `password_input_placeholder` | Your password | -| `button_label` | Sign up | -| `loading_button_label` | Signing up ... | -| `social_provider_text` | Sign in with `{{provider}}` | -| `link_text` | Don't have an account? Sign up | -| `confirmation_text` | Check your email for the confirmation link | - - - - - -| Label Tag | Default Label | -| ---------------------------- | -------------------------------- | -| `email_label` | Email address | -| `password_label` | Your Password | -| `email_input_placeholder` | Your email address | -| `password_input_placeholder` | Your password | -| `button_label` | Sign in | -| `loading_button_label` | Signing in ... | -| `social_provider_text` | Sign in with `{{provider}}` | -| `link_text` | Already have an account? Sign in | - - - - - -| Label Tag | Default Label | -| ------------------------- | ----------------------------------- | -| `email_input_label` | Email address | -| `email_input_placeholder` | Your email address | -| `button_label` | Sign in | -| `loading_button_label` | Signing in ... | -| `link_text` | Send a magic link email | -| `confirmation_text` | Check your email for the magic link | - - - - - -| Label Tag | Default Label | -| ------------------------- | -------------------------------------------- | -| `email_label` | Email address | -| `password_label` | Your Password | -| `email_input_placeholder` | Your email address | -| `button_label` | Send reset password instructions | -| `loading_button_label` | Sending reset instructions ... | -| `link_text` | Forgot your password? | -| `confirmation_text` | Check your email for the password reset link | - - - - - -| Label Tag | Default Label | -| ---------------------------- | ------------------------------ | -| `password_label` | New Password | -| `password_input_placeholder` | Your new password | -| `button_label` | Update password | -| `loading_button_label` | Updating password ... | -| `confirmation_text` | Your password has been updated | - - - - - -| Label Tag | Default Label | -| ------------------------- | ------------------ | -| `email_input_label` | Email address | -| `email_input_placeholder` | Your email address | -| `phone_input_label` | Phone number | -| `phone_input_placeholder` | Your phone number | -| `token_input_label` | Token | -| `token_input_placeholder` | Your OTP token | -| `button_label` | Verify token | -| `loading_button_label` | Signing in ... | - - - - - - - -Currently, translating error messages (e.g. "Invalid credentials") is not supported. Check [related issue.](https://github.com/supabase-community/auth-ui/issues/86) - - - -### Hiding links [#hiding-links] - -You can hide links by setting the `showLinks` prop to `false` - -```js -import { createClient } from '@supabase/supabase-js' -import { Auth } from '@supabase/auth-ui-react' - -const supabase = createClient('', '') - -const App = () => -``` - -Setting `showLinks` to `false` will hide the following links: - -- Don't have an account? Sign up -- Already have an account? Sign in -- Send a magic link email -- Forgot your password? - -### Sign in and sign up views - -Add `sign_in` or `sign_up` views with the `view` prop: - -``` - -``` diff --git a/apps/docs/content/guides/auth/auth-helpers/flutter-auth-ui.mdx b/apps/docs/content/guides/auth/auth-helpers/flutter-auth-ui.mdx deleted file mode 100644 index 2c82ecfb49674..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/flutter-auth-ui.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -id: 'flutter-auth-ui' -title: 'Flutter Auth UI' -description: 'Prebuilt, customizable Flutter widgets for authenticating users.' -sitemapPriority: 0.3 ---- - -Flutter Auth UI is a Flutter package containing pre-built widgets for authenticating users. -It is unstyled and can match your brand and aesthetic. - -![Flutter Auth UI](https://raw.githubusercontent.com/supabase-community/flutter-auth-ui/main/screenshots/supabase_auth_ui.png) - -## Add Flutter Auth UI - -Add the latest version of the package [supabase-auth-ui](https://pub.dev/packages/supabase_auth_ui) to pubspec.yaml: - -```bash -flutter pub add supabase_auth_ui -``` - -### Initialize the Flutter Auth package - -```dart -import 'package:flutter/material.dart'; -import 'package:supabase_auth_ui/supabase_auth_ui.dart'; - -void main() async { - await Supabase.initialize( - url: dotenv.get('SUPABASE_URL'), - anonKey: dotenv.get('SUPABASE_PUBLISHABLE_KEY'), - ); - - runApp(const MyApp()); -} -``` - -### Email Auth - -Use a `SupaEmailAuth` widget to create an email and password signin and signup form. It also contains a button to toggle to display a forgot password form. - -You can pass `metadataFields` to add additional fields to the form to pass as metadata to Supabase. - -```dart -SupaEmailAuth( - redirectTo: kIsWeb ? null : 'io.mydomain.myapp://callback', - onSignInComplete: (response) {}, - onSignUpComplete: (response) {}, - metadataFields: [ - MetaDataField( - prefixIcon: const Icon(Icons.person), - label: 'Username', - key: 'username', - validator: (val) { - if (val == null || val.isEmpty) { - return 'Please enter something'; - } - return null; - }, - ), - ], -) -``` - -### Magic link Auth - -Use `SupaMagicAuth` widget to create a magic link signIn form. - -```dart -SupaMagicAuth( - redirectUrl: kIsWeb ? null : 'io.mydomain.myapp://callback', - onSuccess: (Session response) {}, - onError: (error) {}, -) -``` - -### Reset password - -Use `SupaResetPassword` to create a password reset form. - -```dart -SupaResetPassword( - accessToken: supabase.auth.currentSession?.accessToken, - onSuccess: (UserResponse response) {}, - onError: (error) {}, -) -``` - -### Phone Auth - -Use `SupaPhoneAuth` to create a phone authentication form. - -```dart -SupaPhoneAuth( - authAction: SupaAuthAction.signUp, - onSuccess: (AuthResponse response) {}, -), -``` - -### Social Auth - -The package supports login with [official social providers](../../auth#providers). - -Use `SupaSocialsAuth` to create list of social login buttons. - -```dart -SupaSocialsAuth( - socialProviders: [ - OAuthProvider.apple, - OAuthProvider.google, - ], - colored: true, - redirectUrl: kIsWeb - ? null - : 'io.mydomain.myapp://callback', - onSuccess: (Session response) {}, - onError: (error) {}, -) -``` - -### Theming - -This package uses plain Flutter components allowing you to control the appearance of the components using your own theme. diff --git a/apps/docs/content/guides/auth/auth-helpers/nextjs-pages.mdx b/apps/docs/content/guides/auth/auth-helpers/nextjs-pages.mdx deleted file mode 100644 index 71e00402d785c..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/nextjs-pages.mdx +++ /dev/null @@ -1,941 +0,0 @@ ---- -id: 'nextjs-pages' -title: 'Supabase Auth with Next.js Pages Directory' -description: 'Authentication helpers for Next.js API routes, middleware, and SSR in the Pages Directory.' -sidebar_label: 'Next.js (pages)' -sitemapPriority: 0.3 ---- - - - -The Auth helpers package is deprecated. Use the new `@supabase/ssr` package for Server Side Authentication. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Read the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - -We recommend setting up Auth for your Next.js app with `@supabase/ssr` instead. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs?router=pages) to learn how. - - - - - - - -This submodule provides convenience helpers for implementing user authentication in Next.js applications using the pages directory. - - - -Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the App Router has reached stable status. This is now the recommended path for new Next.js app. Check out our guide on using [Auth Helpers with the Next.js App Directory](/docs/guides/auth/auth-helpers/nextjs). - - - -## Install the Next.js helper library - -```sh Terminal -npm install @supabase/auth-helpers-nextjs @supabase/supabase-js -``` - -This library supports the following tooling versions: - -- Node.js: `^10.13.0 || >=12.0.0` -- Next.js: `>=10` - -Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. - -```sh Terminal -npm install @supabase/auth-helpers-react -``` - -## Set up environment variables - -Retrieve your project URL and anon key in your project's [API settings](/dashboard/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example). - -```bash .env.local -NEXT_PUBLIC_SUPABASE_URL=your-supabase-url -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-supabase-publishable-key -``` - -## Basic setup - - - - -Wrap your `pages/_app.js` component with the `SessionContextProvider` component: - -```jsx pages/_app.js -import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' -import { SessionContextProvider } from '@supabase/auth-helpers-react' -import { useState } from 'react' - -function MyApp({ Component, pageProps }) { - // Create a new supabase browser client on every first render. - const [supabaseClient] = useState(() => createPagesBrowserClient()) - - return ( - - - - ) -} -``` - - - - -Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: - -```tsx -import { type AppProps } from 'next/app' -import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' -import { SessionContextProvider, Session } from '@supabase/auth-helpers-react' -import { useState } from 'react' - -function MyApp({ - Component, - pageProps, -}: AppProps<{ - initialSession: Session -}>) { - // Create a new supabase browser client on every first render. - const [supabaseClient] = useState(() => createPagesBrowserClient()) - - return ( - - - - ) -} -export default MyApp -``` - - - - -You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. - -### Code Exchange API route - -The `Code Exchange` API route is required for the [server-side auth flow](/docs/guides/auth/server-side-rendering) implemented by the Next.js Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. - - - - -Create a new file at `pages/api/auth/callback.js` and populate with the following: - -```jsx pages/api/auth/callback.js -import { NextApiHandler } from 'next' -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -const handler = async (req, res) => { - const { code } = req.query - - if (code) { - const supabase = createPagesServerClient({ req, res }) - await supabase.auth.exchangeCodeForSession(String(code)) - } - - res.redirect('/') -} - -export default handler -``` - - - - - -Create a new file at `pages/api/auth/callback.ts` and populate with the following: - -```tsx pages/api/auth/callback.ts -import { NextApiHandler } from 'next' -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -const handler: NextApiHandler = async (req, res) => { - const { code } = req.query - - if (code) { - const supabase = createPagesServerClient({ req, res }) - await supabase.auth.exchangeCodeForSession(String(code)) - } - - res.redirect('/') -} - -export default handler -``` - - - - -## Usage with TypeScript - -You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion: - -### Browser client - -Creating a new `supabase` client object: - -```tsx -import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' -import { Database } from '../database.types' - -const supabaseClient = createPagesBrowserClient() -``` - -Retrieving a `supabase` client object from the `SessionContext`: - -```tsx -import { useSupabaseClient } from '@supabase/auth-helpers-react' -import { Database } from '../database.types' - -const supabaseClient = useSupabaseClient() -``` - -### Server client - -```tsx -// Creating a new supabase server client object (e.g. in API route): -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import type { NextApiRequest, NextApiResponse } from 'next' -import type { Database } from 'types_db' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const supabaseServerClient = createPagesServerClient({ - req, - res, - }) - const { - data: { user }, - } = await supabaseServerClient.auth.getUser() - - res.status(200).json({ name: user?.name ?? '' }) -} -``` - -## Client-side data fetching with RLS - -For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook: - -```jsx -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' -import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react' -import { useEffect, useState } from 'react' - -const LoginPage = () => { - const supabaseClient = useSupabaseClient() - const user = useUser() - const [data, setData] = useState() - - useEffect(() => { - async function loadData() { - const { data } = await supabaseClient.from('test').select('*') - setData(data) - } - // Only run query once user is logged in. - if (user) loadData() - }, [user]) - - if (!user) - return ( - - ) - - return ( - <> - -

user:

-
{JSON.stringify(user, null, 2)}
-

client-side data fetching with RLS

-
{JSON.stringify(data, null, 2)}
- - ) -} - -export default LoginPage -``` - -## Server-side rendering (SSR) - -Create a server Supabase client to retrieve the logged in user's session: - -```jsx pages/profile.js -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -export default function Profile({ user }) { - return
Hello {user.name}
-} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a user - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - return { - props: { - user, - }, - } -} -``` - -## Server-side data fetching with RLS - -You can use the server Supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side: - - - - -```jsx -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, data }) { - return ( - <> -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!session) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - // Run queries with RLS on the server - const { data } = await supabase.from('users').select('*') - - return { - props: { - user, - data: data ?? [], - }, - } -} -``` - -
- - -```tsx -import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function ProtectedPage({ user, data }: { user: User; data: any }) { - return ( - <> -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - // Run queries with RLS on the server - const { data } = await supabase.from('users').select('*') - - return { - props: { - user, - data: data ?? [], - }, - } -} -``` - -
-
- -## Server-side data fetching to OAuth APIs using `provider token` {`#oauth-provider-token`} - -When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. - -Note that the server accesses data on the session object returned by `auth.getSession`. This data should normally not be trusted, because it is read from the local storage medium. It is not revalidated against the Auth server unless the session is expired, which means the sender can tamper with it. - -In this case, the third-party API will validate the `provider_token`, and a malicious actor is unable to forge one. - - - - -```jsx -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, allRepos }) { - return ( - <> -
Protected content for {user.email}
-

Data fetched with provider token:

-
{JSON.stringify(allRepos, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a session - const { - data: { session }, - } = await supabase.auth.getSession() - - if (!session) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - // Retrieve provider_token & logged in user's third-party id from metadata - const { provider_token, user } = session - const userId = user.user_metadata.user_name - - const allRepos = await ( - await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, { - method: 'GET', - headers: { - Authorization: `token ${provider_token}`, - }, - }) - ).json() - - return { props: { user, allRepos } } -} -``` - -
- - -```tsx -import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) { - return ( - <> -
Protected content for {user.email}
-

Data fetched with provider token:

-
{JSON.stringify(allRepos, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a session - const { - data: { session }, - } = await supabase.auth.getSession() - - if (!session) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - // Retrieve provider_token & logged in user's third-party id from metadata - const { provider_token, user } = session - const userId = user.user_metadata.user_name - - const allRepos = await ( - await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, { - method: 'GET', - headers: { - Authorization: `token ${provider_token}`, - }, - }) - ).json() - - return { props: { user, allRepos } } -} -``` - -
-
- -## Protecting API routes - -Create a server Supabase client to retrieve the logged in user's session: - - - - -```jsx pages/api/protected-route.js -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient({ req, res }) - // Check if we have a user - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return res.status(401).json({ - error: 'not_authenticated', - description: 'The user does not have an active session or is not authenticated', - }) - - // Run queries with RLS on the server - const { data } = await supabase.from('test').select('*') - res.json(data) -} - -export default ProtectedRoute -``` - - - - -```tsx pages/api/protected-route.ts -import { NextApiHandler } from 'next' -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute: NextApiHandler = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient({ req, res }) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return res.status(401).json({ - error: 'not_authenticated', - description: 'The user does not have an active session or is not authenticated', - }) - - // Run queries with RLS on the server - const { data } = await supabase.from('test').select('*') - res.json(data) -} - -export default ProtectedRoute -``` - - - - -## Auth with Next.js proxy - -As an alternative to protecting individual pages you can use a [Next.js Proxy](https://nextjs.org/docs/app/getting-started/proxy) to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected: - -```ts middleware.ts -import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' - -export async function middleware(req: NextRequest) { - // We need to create a response and hand it to the supabase client to be able to modify the response headers. - const res = NextResponse.next() - // Create authenticated Supabase Client. - const supabase = createMiddlewareClient({ req, res }) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - // Check auth condition - if (user?.email?.endsWith('@gmail.com')) { - // Authentication successful, forward request to protected route. - return res - } - - // Auth condition not met, redirect to home page. - const redirectUrl = req.nextUrl.clone() - redirectUrl.pathname = '/' - redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname) - return NextResponse.redirect(redirectUrl) -} - -export const config = { - matcher: '/middleware-protected/:path*', -} -``` - -## Migration guide - -### Migrating to v0.7.X - -#### PKCE Auth flow - -PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new API route for `/api/auth/callback` that exchanges an auth `code` for the user's `session`. - -Check the [Code Exchange API Route steps](/docs/guides/auth/auth-helpers/nextjs-pages#code-exchange-api-route) above to implement this route. - -#### Authentication - -For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function: - -```jsx -supabase.auth.signUp({ - email: 'valid.email@supabase.io', - password: 'sup3rs3cur3', - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, -}) -``` - -#### Deprecated functions - -With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for `createClient` functions. The `createBrowserSupabaseClient` and `createServerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers. - -- `createBrowserSupabaseClient` has been replaced with `createPagesBrowserClient` -- `createServerSupabaseClient` has been replaced with `createPagesServerClient` - -### Migrating to v0.5.X - -To make these helpers more flexible as well as more maintainable and easier to upgrade for new versions of Next.js, we're stripping them down to the most useful part which is managing the cookies and giving you an authenticated supabase-js client in any environment (client, server, middleware/edge). - -Therefore we're marking the `withApiAuth`, `withPageAuth`, and `withMiddlewareAuth` higher order functions as deprecated and they will be removed in the next **minor** release (v0.6.X). - -Follow the steps below to update your API routes, pages, and middleware handlers. Thanks! - -#### `withApiAuth` deprecated! - -Use `createPagesServerClient` within your `NextApiHandler`: - - - - -```tsx pages/api/protected-route.ts -import { withApiAuth } from '@supabase/auth-helpers-nextjs' - -export default withApiAuth(async function ProtectedRoute(req, res, supabase) { - // Run queries with RLS on the server - const { data } = await supabase.from('test').select('*') - res.json(data) -}) -``` - - - - -```tsx pages/api/protected-route.ts -import { NextApiHandler } from 'next' -import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute: NextApiHandler = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient({ req, res }) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return res.status(401).json({ - error: 'not_authenticated', - description: 'The user does not have an active session or is not authenticated', - }) - - // Run queries with RLS on the server - const { data } = await supabase.from('test').select('*') - res.json(data) -} - -export default ProtectedRoute -``` - - - - -#### `withPageAuth` deprecated! - -Use `createPagesServerClient` within `getServerSideProps`: - - - - -```tsx pages/profile.tsx -import { withPageAuth, User } from '@supabase/auth-helpers-nextjs' - -export default function Profile({ user }: { user: User }) { - return
{JSON.stringify(user, null, 2)}
-} - -export const getServerSideProps = withPageAuth({ redirectTo: '/' }) -``` - -
- - -```tsx pages/profile.js -import { createPagesServerClient, User } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function Profile({ user }: { user: User }) { - return
{JSON.stringify(user, null, 2)}
-} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createPagesServerClient(ctx) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) - return { - redirect: { - destination: '/', - permanent: false, - }, - } - - return { - props: { - initialSession: session, - user: session.user, - }, - } -} -``` - -
-
- -#### `withMiddlewareAuth` deprecated! - - - - -```tsx middleware.ts -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' - -export const middleware = withMiddlewareAuth({ - redirectTo: '/', - authGuard: { - isPermitted: async (user) => { - return user.email?.endsWith('@gmail.com') ?? false - }, - redirectTo: '/insufficient-permissions', - }, -}) - -export const config = { - matcher: '/middleware-protected', -} -``` - - - - -```tsx middleware.ts -import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' - -export async function middleware(req: NextRequest) { - // We need to create a response and hand it to the supabase client to be able to modify the response headers. - const res = NextResponse.next() - // Create authenticated Supabase Client. - const supabase = createMiddlewareClient({ req, res }) - // Check if we have a session - const { - data: { user }, - } = await supabase.auth.getUser() - - // Check auth condition - if (user?.email?.endsWith('@gmail.com')) { - // Authentication successful, forward request to protected route. - return res - } - - // Auth condition not met, redirect to home page. - const redirectUrl = req.nextUrl.clone() - redirectUrl.pathname = '/' - redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname) - return NextResponse.redirect(redirectUrl) -} - -export const config = { - matcher: '/middleware-protected', -} -``` - - - - -### Migrating to v0.4.X and supabase-js v2 - -With the update to `supabase-js` v2 the `auth` API routes are no longer required, therefore you can go ahead and delete your `auth` directory under the `/pages/api/` directory. Refer to the [v2 migration guide](/docs/reference/javascript/v1/upgrade-guide) for the full set of changes within supabase-js. - -The `/api/auth/logout` API route has been removed, use the `signout` method instead: - -```jsx - -``` - -The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createPagesBrowserClient` and `createPagesServerClient` methods. This allows you to provide the CLI-generated types to the client: - -```tsx -// client-side -import type { Database } from 'types_db' -const [supabaseClient] = useState(() => createPagesBrowserClient()) - -// server-side API route -import type { NextApiRequest, NextApiResponse } from 'next' -import type { Database } from 'types_db' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const supabaseServerClient = createPagesServerClient({ - req, - res, - }) - const { - data: { user }, - } = await supabaseServerClient.auth.getUser() - - res.status(200).json({ name: user?.name ?? '' }) -} -``` - -- The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` component with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`. -- The `useUser` hook now returns the `user` object or `null`. -- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion: - -Creating a new `supabase` client object: - -```tsx -import { Database } from '../database.types' - -const [supabaseClient] = useState(() => createPagesBrowserClient()) -``` - -Retrieving a `supabase` client object from the `SessionContext`: - -```tsx -import { useSupabaseClient } from '@supabase/auth-helpers-react' -import { Database } from '../database.types' - -const supabaseClient = useSupabaseClient() -``` - -
- -
diff --git a/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx deleted file mode 100644 index 9eaab4f00aacb..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/nextjs.mdx +++ /dev/null @@ -1,1335 +0,0 @@ ---- -id: 'nextjs' -title: 'Supabase Auth with the Next.js App Router' -description: 'Authentication and Authorization helpers for creating an authenticated Supabase client with the Next.js 13 App Router.' -sidebar_label: 'Next.js' -sitemapPriority: 0.3 ---- - - - -The Auth helpers package is deprecated. Use the new `@supabase/ssr` package for Server Side Authentication. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Read the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - -We recommend setting up Auth for your Next.js app with `@supabase/ssr` instead. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs?router=pages) to learn how. - - - - - - - -The [Next.js Auth Helpers package](https://github.com/supabase/auth-helpers) configures Supabase Auth to store the user's `session` in a `cookie`, rather than `localStorage`. This makes it available across the client and server of the App Router - [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components), [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components), [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions), [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) and [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware). The `session` is automatically sent along with any requests to Supabase. - -
- -
- - - -If you are using the `pages` directory, check out [Auth Helpers in Next.js Pages Directory](/docs/guides/auth/auth-helpers/nextjs-pages). - - - -## Install Next.js Auth helpers library - -```sh Terminal -npm install @supabase/auth-helpers-nextjs @supabase/supabase-js -``` - -## Declare environment variables - -Retrieve your project's URL and anon key from your [API settings](/dashboard/project/_/settings/api), and create a `.env.local` file with the following environment variables: - -```bash .env.local -NEXT_PUBLIC_SUPABASE_URL=your-supabase-url -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-supabase-publishable-key -``` - -## Managing session with middleware - -When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary. - -Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies. - -Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. To avoid unnecessary execution, we include a matcher config to decide when the middleware should run. You can read more on matching paths in the Next.js [documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths). We'll use Middleware to refresh the user's session before loading Server Component routes. - - - - -Create a new `middleware.js` file in the root of your project and populate with the following: - -```js middleware.js -import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' - -export async function middleware(req) { - const res = NextResponse.next() - - // Create a Supabase client configured to use cookies - const supabase = createMiddlewareClient({ req, res }) - - // Refresh session if expired - required for Server Components - await supabase.auth.getUser() - - return res -} - -// Ensure the middleware is only called for relevant paths. -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - * Feel free to modify this pattern to include more paths. - */ - '/((?!_next/static|_next/image|favicon.ico).*)', - ], -} -``` - - - - - -Create a new `middleware.ts` file in the root of your project and populate with the following: - -```ts middleware.ts -import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' - -import type { NextRequest } from 'next/server' -import type { Database } from '@/lib/database.types' - -export async function middleware(req: NextRequest) { - const res = NextResponse.next() - - // Create a Supabase client configured to use cookies - const supabase = createMiddlewareClient({ req, res }) - - // Refresh session if expired - required for Server Components - await supabase.auth.getSession() - - return res -} - -// Ensure the middleware is only called for relevant paths. -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - '/((?!_next/static|_next/image|favicon.ico).*)', - ], -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createMiddlewareClient` to add type support to the Supabase client. - - - - - - - - -The `getSession` function must be called for any Server Component routes that use a Supabase client. - - - -## Managing sign-in with Code Exchange - -The Next.js Auth Helpers are configured to use the [server-side auth flow](/docs/guides/auth/server-side-rendering) to sign users into your application. This requires you to setup a `Code Exchange` route, to exchange an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. - -To make this work with Next.js, we create a callback Route Handler that performs this exchange: - - - - -Create a new file at `app/auth/callback/route.js` and populate with the following: - -```js app/auth/callback/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -export async function GET(request) { - const requestUrl = new URL(request.url) - const code = requestUrl.searchParams.get('code') - - if (code) { - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - await supabase.auth.exchangeCodeForSession(code) - } - - // URL to redirect to after sign in process completes - return NextResponse.redirect(requestUrl.origin) -} -``` - - - - - -Create a new file at `app/auth/callback/route.ts` and populate with the following: - -```ts app/auth/callback/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import type { NextRequest } from 'next/server' -import type { Database } from '@/lib/database.types' - -export async function GET(request: NextRequest) { - const requestUrl = new URL(request.url) - const code = requestUrl.searchParams.get('code') - - if (code) { - const cookieStore = await cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - await supabase.auth.exchangeCodeForSession(code) - } - - // URL to redirect to after sign in process completes - return NextResponse.redirect(requestUrl.origin) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - -## Authentication - -
- -
- -Authentication can be initiated [client](/docs/guides/auth/auth-helpers/nextjs#client-side) or [server-side](/docs/guides/auth/auth-helpers/nextjs#server-side). All of the [supabase-js authentication strategies](/docs/reference/javascript/auth-api) are supported with the Auth Helpers client. - - - -The authentication flow requires the [Code Exchange Route](/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange) to exchange a `code` for the user's `session`. - - - -### Client-side - -Client Components can be used to trigger the authentication process from event handlers. - - - - -```jsx app/login/page.jsx -'use client' - -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useRouter } from 'next/navigation' -import { useState } from 'react' - -export default function Login() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const router = useRouter() - const supabase = createClientComponentClient() - - const handleSignUp = async () => { - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${location.origin}/auth/callback`, - }, - }) - router.refresh() - } - - const handleSignIn = async () => { - await supabase.auth.signInWithPassword({ - email, - password, - }) - router.refresh() - } - - const handleSignOut = async () => { - await supabase.auth.signOut() - router.refresh() - } - - return ( - <> - setEmail(e.target.value)} value={email} /> - setPassword(e.target.value)} - value={password} - /> - - - - - ) -} -``` - - - - - -```tsx app/login/page.tsx -'use client' - -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useRouter } from 'next/navigation' -import { useState } from 'react' - -import type { Database } from '@/lib/database.types' - -export default function Login() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const router = useRouter() - const supabase = createClientComponentClient() - - const handleSignUp = async () => { - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${location.origin}/auth/callback`, - }, - }) - router.refresh() - } - - const handleSignIn = async () => { - await supabase.auth.signInWithPassword({ - email, - password, - }) - router.refresh() - } - - const handleSignOut = async () => { - await supabase.auth.signOut() - router.refresh() - } - - return ( - <> - setEmail(e.target.value)} value={email} /> - setPassword(e.target.value)} - value={password} - /> - - - - - ) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClientComponentClient` to add type support to the Supabase client. - - - - - - -### Server-side - -The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) can be used to trigger the authentication process from form submissions. - -#### Sign up route - - - - -```jsx app/auth/sign-up/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -export async function POST(request) { - const requestUrl = new URL(request.url) - const formData = await request.formData() - const email = formData.get('email') - const password = formData.get('password') - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${requestUrl.origin}/auth/callback`, - }, - }) - - return NextResponse.redirect(requestUrl.origin, { - status: 301, - }) -} -``` - - - - - -```tsx app/auth/sign-up/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import type { Database } from '@/lib/database.types' - -export async function POST(request: Request) { - const requestUrl = new URL(request.url) - const formData = await request.formData() - const email = String(formData.get('email')) - const password = String(formData.get('password')) - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${requestUrl.origin}/auth/callback`, - }, - }) - - return NextResponse.redirect(requestUrl.origin, { - status: 301, - }) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - - - -Returning a `301` status redirects from a POST to a GET route - - - -#### Login route - - - - -```jsx app/auth/login/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -export async function POST(request) { - const requestUrl = new URL(request.url) - const formData = await request.formData() - const email = formData.get('email') - const password = formData.get('password') - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signInWithPassword({ - email, - password, - }) - - return NextResponse.redirect(requestUrl.origin, { - status: 301, - }) -} -``` - - - - - -```tsx app/auth/login/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import type { Database } from '@/lib/database.types' - -export async function POST(request: Request) { - const requestUrl = new URL(request.url) - const formData = await request.formData() - const email = String(formData.get('email')) - const password = String(formData.get('password')) - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signInWithPassword({ - email, - password, - }) - - return NextResponse.redirect(requestUrl.origin, { - status: 301, - }) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - - - -Returning a `301` status redirects from a POST to a GET route - - - -#### Logout route - - - - -```jsx app/auth/logout/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -export async function POST(request) { - const requestUrl = new URL(request.url) - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signOut() - - return NextResponse.redirect(`${requestUrl.origin}/login`, { - status: 301, - }) -} -``` - - - - - -```tsx app/auth/logout/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import type { Database } from '@/lib/database.types' - -export async function POST(request: Request) { - const requestUrl = new URL(request.url) - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - await supabase.auth.signOut() - - return NextResponse.redirect(`${requestUrl.origin}/login`, { - status: 301, - }) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - -#### Login page - - - - -```jsx app/login/page.jsx -export default function Login() { - return ( -
- - - - - - - -
- ) -} -``` - -
- - - -```tsx app/login/page.tsx -export default function Login() { - return ( -
- - - - - - -
- ) -} -``` - -
-
- -## Creating a Supabase client - -There are 5 ways to access the Supabase client with the Next.js Auth Helpers: - -- [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components) — `createClientComponentClient` in Client Components -- [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components) — `createServerComponentClient` in Server Components -- [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions) — `createServerActionClient` in Server Actions -- [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) — `createRouteHandlerClient` in Route Handlers -- [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware) — `createMiddlewareClient` in Middleware - -This allows for the Supabase client to be instantiated in the correct context. All you need to change is the context in the middle `create[ClientComponent|ServerComponent|ServerAction|RouteHandler|Middleware]Client` and the Auth Helpers will take care of the rest. - -### Client components - -
- -
- -[Client Components](https://nextjs.org/docs/getting-started/react-essentials#client-components) allow the use of client-side hooks - such as `useEffect` and `useState`. They can be used to request data from Supabase client-side, and [subscribe to realtime events](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx). - - - - -```jsx app/client/page.jsx -'use client' - -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useEffect, useState } from 'react' - -export default function Page() { - const [todos, setTodos] = useState() - const supabase = createClientComponentClient() - - useEffect(() => { - const getData = async () => { - const { data } = await supabase.from('todos').select() - setTodos(data) - } - - getData() - }, []) - - return todos ?
{JSON.stringify(todos, null, 2)}
:

Loading todos...

-} -``` - -
- - - -```tsx app/client/page.tsx -'use client' - -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useEffect, useState } from 'react' - -import type { Database } from '@/lib/database.types' - -type Todo = Database['public']['Tables']['todos']['Row'] - -export default function Page() { - const [todos, setTodos] = useState(null) - const supabase = createClientComponentClient() - - useEffect(() => { - const getData = async () => { - const { data } = await supabase.from('todos').select() - setTodos(data) - } - - getData() - }, []) - - return todos ?
{JSON.stringify(todos, null, 2)}
:

Loading todos...

-} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClientComponentClient` to add type support to the Supabase client. - - - -
-
- - - -Check out the [Next.js auth example repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for more examples, including [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx). - - - -#### Singleton - -The `createClientComponentClient` function implements a [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) by default, meaning that all invocations will return the same Supabase client instance. If you need multiple Supabase instances across Client Components, you can pass an additional configuration option `{ isSingleton: false }` to get a new client every time this function is called. - -```jsx -const supabase = createClientComponentClient({ isSingleton: false }) -``` - -### Server components - -
- -
- -[Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) allow for asynchronous data to be fetched server-side. - - - -In order to use Supabase in Server Components, you need to have implemented the [Middleware](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) steps above. - - - - - - -```jsx app/page.jsx -import { cookies } from 'next/headers' -import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' - -export default async function Page() { - const cookieStore = cookies() - const supabase = createServerComponentClient({ cookies: () => cookieStore }) - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - -
- - - -```tsx app/page.tsx -import { cookies } from 'next/headers' -import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' - -import type { Database } from '@/lib/database.types' - -export default async function ServerComponent() { - const cookieStore = cookies() - const supabase = createServerComponentClient({ cookies: () => cookieStore }) - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerComponentClient` to add type support to the Supabase client. - - - -
-
- - - -Check out the [Next.js auth example repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for more examples, including redirecting unauthenticated users - [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/[id]/page.tsx). - - - -### Server actions - -
- -
- -[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) allow mutations to be performed server-side. - - - -Next.js Server Actions are currently in `alpha` so may change without notice. - - - - - - -```jsx app/new-post/page.jsx -import { cookies } from 'next/headers' -import { createServerActionClient } from '@supabase/auth-helpers-nextjs' -import { revalidatePath } from 'next/cache' - -export default async function NewTodo() { - const addTodo = async (formData) => { - 'use server' - - const title = formData.get('title') - const supabase = createServerActionClient({ cookies }) - await supabase.from('todos').insert({ title }) - revalidatePath('/') - } - - return ( -
- -
- ) -} -``` - -
- - - -```tsx app/new-post/page.tsx -import { cookies } from 'next/headers' -import { createServerActionClient } from '@supabase/auth-helpers-nextjs' -import { revalidatePath } from 'next/cache' - -import type { Database } from '@/lib/database.types' - -export default async function NewTodo() { - const addTodo = async (formData: FormData) => { - 'use server' - - const title = formData.get('title') - const cookieStore = cookies() - const supabase = createServerActionClient({ cookies: () => cookieStore }) - await supabase.from('todos').insert({ title }) - revalidatePath('/') - } - - return ( -
- -
- ) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerActionClient` to add type support to the Supabase client. - - - -
-
- -### Route handlers - -
- -
- -[Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) replace API Routes and allow for logic to be performed server-side. They can respond to `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS` requests. - - - - -```jsx app/api/todos/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' - -export async function POST(request) { - const { title } = await request.json() - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - - - -```tsx app/api/todos/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' - -import type { Database } from '@/lib/database.types' - -export async function POST(request: Request) { - const { title } = await request.json() - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - -### Middleware - -See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) above. - -### Edge runtime - -The Next.js Edge Runtime allows you to host Server Components and Route Handlers from Edge nodes, serving the routes as close as possible to your user's location. - -A route can be configured to use the Edge Runtime by exporting a `runtime` variable set to `edge`. Additionally, the `cookies()` function must be called from the Edge route, before creating a Supabase Client. - -#### Server components - - - - -```jsx app/page.jsx -import { cookies } from 'next/headers' -import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' - -export const runtime = 'edge' -export const dynamic = 'force-dynamic' - -export default async function Page() { - const cookieStore = cookies() - - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }) - - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - -
- - - -```tsx app/page.tsx -import { cookies } from 'next/headers' -import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' - -import type { Database } from '@/lib/database.types' - -export const runtime = 'edge' -export const dynamic = 'force-dynamic' - -export default async function Page() { - const cookieStore = cookies() - - const supabase = createServerComponentClient({ - cookies: () => cookieStore, - }) - - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerComponentClient` to add type support to the Supabase client. - - - -
-
- -#### Route handlers - - - - -```jsx app/api/todos/route.js -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' - -export const runtime = 'edge' -export const dynamic = 'force-dynamic' - -export async function POST(request) { - const { title } = await request.json() - const cookieStore = cookies() - const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - - - -```tsx app/api/todos/route.ts -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' - -import type { Database } from '@/lib/database.types' - -export const runtime = 'edge' -export const dynamic = 'force-dynamic' - -export async function POST(request: Request) { - const { title } = await request.json() - const cookieStore = cookies() - - const supabase = createRouteHandlerClient({ - cookies: () => cookieStore, - }) - - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. - - - - - - -### Static routes - -Server Components and Route Handlers are static by default - data is fetched once at build time and the value is cached. Since the request to Supabase now happens at build time, there is no user, session or cookie to pass along with the request to Supabase. Therefore, the `createClient` function from `supabase-js` can be used to fetch data for static routes. - -#### Server components - - - - -```jsx app/page.jsx -import { createClient } from '@supabase/supabase-js' - -export default async function Page() { - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY - ) - - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - -
- - - -```tsx app/page.tsx -import { createClient } from '@supabase/supabase-js' - -import type { Database } from '@/lib/database.types' - -export default async function Page() { - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! - ) - - const { data } = await supabase.from('todos').select() - return
{JSON.stringify(data, null, 2)}
-} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client. - - - -
-
- -#### Route handlers - - - - -```jsx app/api/todos/route.js -import { createClient } from '@supabase/supabase-js' -import { NextResponse } from 'next/server' - -export async function POST(request) { - const { title } = await request.json() - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY - ) - - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - - - -```tsx app/api/todos/route.ts -import { createClient } from '@supabase/supabase-js' -import { NextResponse } from 'next/server' - -import type { Database } from '@/lib/database.types' - -export async function POST(request: Request) { - const { title } = await request.json() - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! - ) - - const { data } = await supabase.from('todos').insert({ title }).select() - return NextResponse.json(data) -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client. - - - - - - -## More examples - -- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb) -- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF) -- [Full App Router example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) -- [Realtime Subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) -- [Protected Routes](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/[id]/page.tsx) -- [Conditional Rendering in Client Components with SSR](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login-form.tsx) - -## Migration guide - -### Migrating to v0.7.X - -#### PKCE Auth flow - -PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new Route Handler for `/auth/callback` that exchanges an auth `code` for the user's `session`. - -Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange) above to implement this Route Handler. - -#### Authentication - -For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange Route Handler - `/auth/callback`. This is an example with the `signUp` function: - -```jsx -supabase.auth.signUp({ - email: 'valid.email@supabase.io', - password: 'sup3rs3cur3', - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, -}) -``` - -#### Deprecated functions - -With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for `createClient` functions. The `createMiddlewareSupabaseClient`, `createBrowserSupabaseClient`, `createServerComponentSupabaseClient` and `createRouteHandlerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers. - -- `createMiddlewareSupabaseClient` has been replaced with `createMiddlewareClient` -- `createBrowserSupabaseClient` has been replaced with `createClientComponentClient` -- `createServerComponentSupabaseClient` has been replaced with `createServerComponentClient` -- `createRouteHandlerSupabaseClient` has been replaced with `createRouteHandlerClient` - -#### `createClientComponentClient` returns singleton - -You no longer need to implement logic to ensure there is only a single instance of the Supabase Client shared across all Client Components - this is now the default and handled by the `createClientComponentClient` function. Call it as many times as you want! - -```jsx -"use client"; - -import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; - -export default function() { - const supabase = createClientComponentClient(); - return ... -} -``` - -For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above. - -
- -
diff --git a/apps/docs/content/guides/auth/auth-helpers/remix.mdx b/apps/docs/content/guides/auth/auth-helpers/remix.mdx deleted file mode 100644 index a8b7074c9ec87..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/remix.mdx +++ /dev/null @@ -1,855 +0,0 @@ ---- -id: 'remix' -title: 'Supabase Auth with Remix' -description: 'Authentication helpers for loaders and actions in Remix.' -sidebar_label: 'Remix' -sitemapPriority: 0.3 ---- - - - -The Auth helpers package is deprecated. Use the new `@supabase/ssr` package for Server Side Authentication. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Read the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - - - - - - - -This submodule provides convenience helpers for implementing user authentication in Remix applications. - -
- -
- - - -For a complete implementation example, check out [this free egghead course](https://egghead.io/courses/build-a-realtime-chat-app-with-remix-and-supabase-d36e2618) or [this GitHub repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix). - - - -## Install the Remix helper library - -```sh Terminal -npm install @supabase/auth-helpers-remix @supabase/supabase-js -``` - -This library supports the following tooling versions: - -- Remix: `>=1.7.2` - -## Set up environment variables - -Retrieve your project URL and anon key in your project's [API settings](/dashboard/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/remix/.env.example). - -```bash .env -SUPABASE_URL=YOUR_SUPABASE_URL -SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY -``` - -### Code Exchange route - -The `Code Exchange` route is required for the [server-side auth flow](/docs/guides/auth/server-side-rendering) implemented by the Remix Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. - - - - -Create a new file at `app/routes/auth.callback.jsx` and populate with the following: - -```jsx app/routes/auth.callback.jsx -import { redirect } from '@remix-run/node' -import { createServerClient } from '@supabase/auth-helpers-remix' - -export const loader = async ({ request }) => { - const response = new Response() - const url = new URL(request.url) - const code = url.searchParams.get('code') - - if (code) { - const supabaseClient = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_PUBLISHABLE_KEY, - { request, response } - ) - await supabaseClient.auth.exchangeCodeForSession(code) - } - - return redirect('/', { - headers: response.headers, - }) -} -``` - - - - - -Create a new file at `app/routes/auth.callback.tsx` and populate with the following: - -```tsx app/routes/auth.callback.tsx -import { redirect } from '@remix-run/node' -import { createServerClient } from '@supabase/auth-helpers-remix' - -import type { Database } from 'db_types' -import type { LoaderFunctionArgs } from '@remix-run/node' - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const response = new Response() - const url = new URL(request.url) - const code = url.searchParams.get('code') - - if (code) { - const supabaseClient = createServerClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_PUBLISHABLE_KEY!, - { request, response } - ) - await supabaseClient.auth.exchangeCodeForSession(code) - } - - return redirect('/', { - headers: response.headers, - }) -} -``` - -> `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types). - - - - -## Server-side - -The Supabase client can now be used server-side - in loaders and actions - by calling the `createServerClient` function. - -### Loader - -Loader functions run on the server immediately before the component is rendered. They respond to all GET requests on a route. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, and a `Request` and `Response`. - - - - -```jsx -import { json } from '@remix-run/node' // change this import to whatever runtime you are using -import { createServerClient } from '@supabase/auth-helpers-remix' - -export const loader = async ({ request }) => { - const response = new Response() - // an empty response is required for the auth helpers - // to set cookies to manage auth - - const supabaseClient = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_PUBLISHABLE_KEY, - { request, response } - ) - - const { data } = await supabaseClient.from('test').select('*') - - // in order for the set-cookie header to be set, - // headers must be returned as part of the loader response - return json( - { data }, - { - headers: response.headers, - } - ) -} -``` - - - -Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function. - - - - - - -```jsx -import { json } from '@remix-run/node' // change this import to whatever runtime you are using -import { createServerClient } from '@supabase/auth-helpers-remix' - -import type { LoaderFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const response = new Response() - const supabaseClient = createServerClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_PUBLISHABLE_KEY!, - { request, response } - ) - - const { data } = await supabaseClient.from('test').select('*') - - return json( - { data }, - { - headers: response.headers, - } - ) -} -``` - - - -Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function. - - - - - - -### Action - -Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the `createServerClient` function and passing it your `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, and a `Request` and `Response`. - - - - -```jsx -import { json } from '@remix-run/node' // change this import to whatever runtime you are using -import { createServerClient } from '@supabase/auth-helpers-remix' - -export const action = async ({ request }) => { - const response = new Response() - - const supabaseClient = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_PUBLISHABLE_KEY, - { request, response } - ) - - const { data } = await supabaseClient.from('test').select('*') - - return json( - { data }, - { - headers: response.headers, - } - ) -} -``` - - - -Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function. - - - - - - -```jsx -import { json } from '@remix-run/node' // change this import to whatever runtime you are using -import { createServerClient } from '@supabase/auth-helpers-remix' - -import type { ActionFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using - -export const action = async ({ request }: ActionFunctionArgs) => { - const response = new Response() - - const supabaseClient = createServerClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_PUBLISHABLE_KEY!, - { request, response } - ) - - const { data } = await supabaseClient.from('test').select('*') - - return json( - { data }, - { - headers: response.headers, - } - ) -} -``` - - - -Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function. - - - - - - -## Session and user - -You can determine if a user is authenticated by checking their session using the `getSession` function. - -```jsx -const { - data: { session }, -} = await supabaseClient.auth.getSession() -``` - -The session contains a user property. This is the user metadata saved, unencoded, to the local storage medium. It's unverified and can be tampered by the user, so don't use it for authorization or sensitive purposes. - -<$Partial path="get_session_warning.mdx" /> - -```jsx -const user = session?.user -``` - -Or, if you need trusted user data, you can call the `getUser()` function, which retrieves the trusted user data by making a request to the Supabase Auth server. - -```jsx -const { - data: { user }, -} = await supabaseClient.auth.getUser() -``` - -## Client-side - -We still need to use Supabase client-side for things like authentication and realtime subscriptions. Anytime we use Supabase client-side it needs to be a single instance. - -### Creating a singleton Supabase client - -Since our environment variables are not available client-side, we need to plumb them through from the loader. - - - - -```jsx app/root.jsx -export const loader = () => { - const env = { - SUPABASE_URL: process.env.SUPABASE_URL, - SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY, - } - - return json({ env }) -} -``` - - - -These may not be stored in `process.env` for environments other than Node. - - - -Next, we call the `useLoaderData` hook in our component to get the `env` object. - -```jsx app/root.jsx -const { env } = useLoaderData() -``` - -We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components. - -```jsx app/root.jsx -const [supabase] = useState(() => - createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY) -) -``` - -And then we can share this instance across our application with Outlet Context. - -```jsx app/root.jsx - -``` - - - - -```tsx app/root.tsx -export const loader = ({}: LoaderFunctionArgs) => { - const env = { - SUPABASE_URL: process.env.SUPABASE_URL!, - SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!, - } - - return json({ env }) -} -``` - - - -These may not be stored in `process.env` for environments other than Node. - - - -Next, we call the `useLoaderData` hook in our component to get the `env` object. - -```tsx app/root.tsx -const { env } = useLoaderData() -``` - -We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components. - -```tsx app/root.tsx -const [supabase] = useState(() => - createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY) -) -``` - -And then we can share this instance across our application with Outlet Context. - -```tsx app/root.tsx - -``` - - - - -### Syncing server and client state - -Since authentication happens client-side, we need to tell Remix to re-call all active loaders when the user signs in or out. - -Remix provides a hook `useRevalidator` that can be used to revalidate all loaders on the current route. - -Now to determine when to submit a post request to this action, we need to compare the server and client state for the user's access token. - -Let's pipe that through from our loader. - - - - - -```jsx app/root.jsx -export const loader = async ({ request }) => { - const env = { - SUPABASE_URL: process.env.SUPABASE_URL, - SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY, - } - - const response = new Response() - - const supabase = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_PUBLISHABLE_KEY, - { - request, - response, - } - ) - - const { - data: { session }, - } = await supabase.auth.getSession() - - return json( - { - env, - session, - }, - { - headers: response.headers, - } - ) -} -``` - - - - - -```tsx app/root.tsx -export const loader = async ({ request }: LoaderFunctionArgs) => { - const env = { - SUPABASE_URL: process.env.SUPABASE_URL!, - SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!, - } - - const response = new Response() - - const supabase = createServerClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_PUBLISHABLE_KEY!, - { - request, - response, - } - ) - - const { - data: { session }, - } = await supabase.auth.getSession() - - return json( - { - env, - session, - }, - { - headers: response.headers, - } - ) -} -``` - - - - - -And then use the revalidator, inside the `onAuthStateChange` hook. - - - - - -```jsx app/root.jsx -const { env, session } = useLoaderData() -const { revalidate } = useRevalidator() - -const [supabase] = useState(() => - createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY) -) - -const serverAccessToken = session?.access_token - -useEffect(() => { - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((event, session) => { - if (session?.access_token !== serverAccessToken) { - // server and client are out of sync. - revalidate() - } - }) - - return () => { - subscription.unsubscribe() - } -}, [serverAccessToken, supabase, revalidate]) -``` - - - - - -```tsx app/root.tsx -const { env, session } = useLoaderData() -const { revalidate } = useRevalidator() - -const [supabase] = useState(() => - createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY) -) - -const serverAccessToken = session?.access_token - -useEffect(() => { - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((event, session) => { - if (event !== 'INITIAL_SESSION' && session?.access_token !== serverAccessToken) { - // server and client are out of sync. - revalidate() - } - }) - - return () => { - subscription.unsubscribe() - } -}, [serverAccessToken, supabase, revalidate]) -``` - - - - - - - -Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) for full implementation example - - - -### Authentication - -Now we can use our outlet context to access our single instance of Supabase and use any of the [supported authentication strategies from `supabase-js`](/docs/reference/javascript/auth-signup). - - - - - -```jsx app/components/login.jsx -export default function Login() { - const { supabase } = useOutletContext() - - const handleEmailLogin = async () => { - await supabase.auth.signInWithPassword({ - email: 'valid.email@supabase.io', - password: 'password', - }) - } - - const handleGitHubLogin = async () => { - await supabase.auth.signInWithOAuth({ - provider: 'github', - options: { - redirectTo: 'http://localhost:3000/auth/callback', - }, - }) - } - - const handleLogout = async () => { - await supabase.auth.signOut() - } - - return ( - <> - - - - - ) -} -``` - - - - - -```tsx app/components/login.tsx -export default function Login() { - const { supabase } = useOutletContext<{ supabase: SupabaseClient }>() - - const handleEmailLogin = async () => { - await supabase.auth.signInWithPassword({ - email: 'valid.email@supabase.io', - password: 'password', - }) - } - - const handleGitHubLogin = async () => { - await supabase.auth.signInWithOAuth({ - provider: 'github', - options: { - redirectTo: 'http://localhost:3000/auth/callback', - }, - }) - } - - const handleLogout = async () => { - await supabase.auth.signOut() - } - - return ( - <> - - - - - ) -} -``` - - - - - -### Subscribe to realtime events - - - - - -```jsx app/routes/realtime.jsx -import { useLoaderData, useOutletContext } from '@remix-run/react' -import { createServerClient } from '@supabase/auth-helpers-remix' -import { json } from '@remix-run/node' -import { useEffect, useState } from 'react' - -export const loader = async ({ request }) => { - const response = new Response() - const supabase = createServerClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_PUBLISHABLE_KEY, - { - request, - response, - } - ) - - const { data } = await supabase.from('posts').select() - - return json({ serverPosts: data ?? [] }, { headers: response.headers }) -} - -export default function Index() { - const { serverPosts } = useLoaderData() - const [posts, setPosts] = useState(serverPosts) - const { supabase } = useOutletContext() - - useEffect(() => { - setPosts(serverPosts) - }, [serverPosts]) - - useEffect(() => { - const channel = supabase - .channel('*') - .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => - setPosts([...posts, payload.new]) - ) - .subscribe() - - return () => { - supabase.removeChannel(channel) - } - }, [supabase, posts, setPosts]) - - return
{JSON.stringify(posts, null, 2)}
-} -``` - -
- - - -```tsx app/routes/realtime.tsx -import { useLoaderData, useOutletContext } from '@remix-run/react' -import { createServerClient } from '@supabase/auth-helpers-remix' -import { json } from '@remix-run/node' -import { useEffect, useState } from 'react' - -import type { SupabaseClient } from '@supabase/auth-helpers-remix' -import type { Database } from 'db_types' - -type Post = Database['public']['Tables']['posts']['Row'] - -import type { LoaderFunctionArgs } from '@remix-run/node' - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const response = new Response() - const supabase = createServerClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_PUBLISHABLE_KEY!, - { - request, - response, - } - ) - - const { data } = await supabase.from('posts').select() - - return json({ serverPosts: data ?? [] }, { headers: response.headers }) -} - -export default function Index() { - const { serverPosts } = useLoaderData() - const [posts, setPosts] = useState(serverPosts) - const { supabase } = useOutletContext<{ supabase: SupabaseClient }>() - - useEffect(() => { - setPosts(serverPosts) - }, [serverPosts]) - - useEffect(() => { - const channel = supabase - .channel('*') - .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => - setPosts([...posts, payload.new as Post]) - ) - .subscribe() - - return () => { - supabase.removeChannel(channel) - } - }, [supabase, posts, setPosts]) - - return
{JSON.stringify(posts, null, 2)}
-} -``` - -> `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types). - -
- -
- - - -Ensure you have [enabled replication](/dashboard/project/_/database/publications) on the table you are subscribing to. - - - -## Migration guide - -### Migrating to v0.2.0 - -#### PKCE Auth flow - -PKCE is the new server-side auth flow implemented by the Remix Auth Helpers. It requires a new `loader` route for `/auth/callback` that exchanges an auth `code` for the user's `session`. - -Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/remix#code-exchange-route) above to implement this route. - -#### Authentication - -For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function: - -```jsx -supabaseClient.auth.signUp({ - email: 'valid.email@supabase.io', - password: 'sup3rs3cur3', - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, -}) -``` - -
- -
diff --git a/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx deleted file mode 100644 index 9a2e26f135499..0000000000000 --- a/apps/docs/content/guides/auth/auth-helpers/sveltekit.mdx +++ /dev/null @@ -1,2051 +0,0 @@ ---- -id: 'sveltekit' -title: 'Supabase Auth with SvelteKit' -description: 'Convenience helpers for implementing user authentication in SvelteKit.' -sidebar_label: 'SvelteKit' -sitemapPriority: 0.3 ---- - - - -The Auth helpers package is deprecated. Use the new `@supabase/ssr` package for Server Side Authentication. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Read the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more. - - - - - - - -This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications. - -## Configuration - -### Install SvelteKit Auth helpers library - -This library supports Node.js `^16.15.0`. - -```sh Terminal -npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js -``` - -### Declare environment variables - -Retrieve your project's URL and anon key from your [API settings](/dashboard/project/_/settings/api), and create a `.env.local` file with the following environment variables: - -```bash .env.local -# Find these in your Supabase project settings https://supabase.com/dashboard/project/_/settings/api -PUBLIC_SUPABASE_URL=https://your-project.supabase.co -PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... or anon key -``` - -### Creating a Supabase client - - - - -Create a new `hooks.server.js` file in the root of your project and populate with the following to retrieve the user session. - -<$Partial path="get_session_warning.mdx" /> - -```js src/hooks.server.js -// src/hooks.server.js -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public' -import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' - -export const handle = async ({ event, resolve }) => { - event.locals.supabase = createSupabaseServerClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event, - }) - - /** - * Unlike `supabase.auth.getSession`, which is unsafe on the server because it - * doesn't validate the JWT, this function validates the JWT by first calling - * `getUser` and aborts early if the JWT signature is invalid. - */ - event.locals.safeGetSession = async () => { - const { - data: { user }, - error, - } = await supabase.auth.getUser() - if (error) { - return { session: null, user: null } - } - - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} -``` - - - - - -Create a new `hooks.server.ts` file in the root of your project and populate with the following: - -```ts src/hooks.server.ts -// src/hooks.server.ts -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public' -import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' -import type { Handle } from '@sveltejs/kit' - -export const handle: Handle = async ({ event, resolve }) => { - event.locals.supabase = createSupabaseServerClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event, - }) - - /** - * Unlike `supabase.auth.getSession`, which is unsafe on the server because it - * doesn't validate the JWT, this function validates the JWT by first calling - * `getUser` and aborts early if the JWT signature is invalid. - */ - event.locals.safeGetSession = async () => { - const { - data: { user }, - error, - } = await supabase.auth.getUser() - if (error) { - return { session: null, user: null } - } - - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} -``` - - - - - - -Note that we are specifying `filterSerializedResponseHeaders` here. We need to tell SvelteKit that Supabase needs the `content-range` and `x-supabase-api-version` headers. - - - -### Code Exchange route - -The `Code Exchange` route is required for the [server-side auth flow](/docs/guides/auth/server-side-rendering) implemented by the SvelteKit Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. - - - - -Create a new file at `src/routes/auth/callback/+server.js` and populate with the following: - -```js src/routes/auth/callback/+server.js -import { redirect } from '@sveltejs/kit' - -export const GET = async ({ url, locals: { supabase } }) => { - const code = url.searchParams.get('code') - - if (code) { - await supabase.auth.exchangeCodeForSession(code) - } - - redirect(303, '/') -} -``` - - - - - -Create a new file at `src/routes/auth/callback/+server.ts` and populate with the following: - -```ts src/routes/auth/callback/+server.ts -import { redirect } from '@sveltejs/kit' - -export const GET = async ({ url, locals: { supabase } }) => { - const code = url.searchParams.get('code') - - if (code) { - await supabase.auth.exchangeCodeForSession(code) - } - - redirect(303, '/') -} -``` - - - - -### Generate types from your database - -In order to get the most out of TypeScript and its IntelliSense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI. - -```ts src/app.d.ts -// src/app.d.ts - -import { SupabaseClient, Session, User } from '@supabase/supabase-js' -import { Database } from './DatabaseDefinitions' - -declare global { - namespace App { - interface Locals { - supabase: SupabaseClient - safeGetSession(): Promise<{ session: Session | null; user: User | null }> - } - interface PageData { - session: Session | null - user: User | null - } - // interface Error {} - // interface Platform {} - } -} -``` - -## Authentication - -Authentication can be initiated [client](/docs/guides/auth/auth-helpers/sveltekit#client-side) or [server-side](/docs/guides/auth/auth-helpers/sveltekit#server-side). All of the [supabase-js authentication strategies](/docs/reference/javascript/auth-api) are supported with the Auth Helpers client. - - - -Note: The authentication flow requires the [Code Exchange Route](/docs/guides/auth/auth-helpers/sveltekit#code-exchange-route) to exchange a `code` for the user's `session`. - - - -### Client-side - -#### Send session to client - -To make the session available across the UI, including pages and layouts, it is crucial to pass the session as a parameter in the root layout's server load function. - - - - -```js src/routes/+layout.server.js -// src/routes/+layout.server.js -export const load = async ({ locals: { safeGetSession } }) => { - const { session, user } = await safeGetSession() - - return { - session, - user, - } -} -``` - - - - -```ts src/routes/+layout.server.ts -// src/routes/+layout.server.ts -export const load = async ({ locals: { safeGetSession } }) => { - const { session, user } = await safeGetSession() - - return { - session, - user, - } -} -``` - - - - -#### Shared load functions and pages - -To utilize Supabase in shared load functions and within pages, it is essential to create a Supabase client in the root layout load. - - - - -```ts src/routes/+layout.js -// src/routes/+layout.js -import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' - -export const load = async ({ fetch, data, depends }) => { - depends('supabase:auth') - - const supabase = createSupabaseLoadClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event: { fetch }, - serverSession: data.session, - }) - - /** - * It's fine to use `getSession` here, because on the client, `getSession` is - * safe, and on the server, it reads `session` from the `LayoutData`, which - * safely checked the session using `safeGetSession`. - */ - const { - data: { session }, - } = await supabase.auth.getSession() - - return { supabase, session } -} -``` - - - - - -```ts src/routes/+layout.ts -// src/routes/+layout.ts -import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' -import type { Database } from '../DatabaseDefinitions' - -export const load = async ({ fetch, data, depends }) => { - depends('supabase:auth') - - const supabase = createSupabaseLoadClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event: { fetch }, - serverSession: data.session, - }) - - /** - * It's fine to use `getSession` here, because on the client, `getSession` is - * safe, and on the server, it reads `session` from the `LayoutData`, which - * safely checked the session using `safeGetSession`. - */ - const { - data: { session }, - } = await supabase.auth.getSession() - - return { supabase, session } -} -``` - - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createSupabaseLoadClient` to add type support to the Supabase client. - - - - - - -Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data`. - -The usage of `depends` tells SvelteKit that this load function should be executed whenever `invalidate` is called to keep the page store in sync. - -`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs. - -#### Setting up the event listener on the client side - -We need to create an event listener in the root `+layout.svelte` file in order to catch Supabase events being triggered. - -```svelte src/routes/+layout.svelte - - - - -``` - -The usage of `invalidate` tells SvelteKit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync. - -#### Sign in / sign up / sign out - -We can access the Supabase instance in our `+page.svelte` file through the data object. - -```svelte src/routes/auth/+page.svelte - - - -
- - - -
- - - -``` - -### Server-side - -[Form Actions](https://kit.svelte.dev/docs/form-actions) can be used to trigger the authentication process from form submissions. - - - - -```js src/routes/login/+page.server.js -// src/routes/login/+page.server.js -import { fail } from '@sveltejs/kit' - -export const actions = { - default: async ({ request, url, locals: { supabase } }) => { - const formData = await request.formData() - const email = formData.get('email') - const password = formData.get('password') - - const { error } = await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${url.origin}/auth/callback`, - }, - }) - - if (error) { - return fail(500, { message: 'Server error. Try again later.', success: false, email }) - } - - return { - message: 'Please check your email for a magic link to log into the website.', - success: true, - } - }, -} -``` - -```svelte src/routes/login/+page.svelte - - - -
- - - -
-``` - -
- - - -```js src/routes/login/+page.server.ts -// src/routes/login/+page.server.ts -import { fail } from '@sveltejs/kit' - -export const actions = { - default: async ({ request, url, locals: { supabase } }) => { - const formData = await request.formData() - const email = formData.get('email') as string - const password = formData.get('password') as string - - const { error } = await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${url.origin}/auth/callback`, - }, - }) - - if (error) { - return fail(500, { message: 'Server error. Try again later.', success: false, email }) - } - - return { - message: 'Please check your email for a magic link to log into the website.', - success: true, - } - }, -} -``` - -```svelte src/routes/login/+page.svelte - - - -
- - - -
-``` - -
-
- -## Authorization - -### Protecting API routes - -Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`. - -```ts src/routes/api/protected-route/+server.ts -// src/routes/api/protected-route/+server.ts -import { json, error } from '@sveltejs/kit' - -export const GET = async ({ locals: { supabase, safeGetSession } }) => { - const { session } = await safeGetSession() - if (!session) { - // the user is not signed in - throw error(401, { message: 'Unauthorized' }) - } - const { data } = await supabase.from('test').select('*') - - return json({ data }) -} -``` - -If you visit `/api/protected-route` without a valid session cookie, you will get a 401 response. - -### Protecting actions - -Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`. - -```ts src/routes/posts/+page.server.ts -// src/routes/posts/+page.server.ts -import { error, fail } from '@sveltejs/kit' - -export const actions = { - createPost: async ({ request, locals: { supabase, safeGetSession } }) => { - const { session } = await safeGetSession() - - if (!session) { - // the user is not signed in - throw error(401, { message: 'Unauthorized' }) - } - // we are save, let the user create the post - const formData = await request.formData() - const content = formData.get('content') - - const { error: createPostError, data: newPost } = await supabase - .from('posts') - .insert({ content }) - - if (createPostError) { - return fail(500, { - supabaseErrorMessage: createPostError.message, - }) - } - return { - newPost, - } - }, -} -``` - -If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 401 error response. - -### Protecting multiple routes - -To avoid writing the same auth logic in every single route you can also use the handle hook to -protect multiple routes at once. For this to work with your Supabase session, you need to use -SvelteKit's [sequence helper](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) function. -Edit your `/src/hooks.server.js` with the below: - - - - -```js src/hooks.server.js -// src/hooks.server.js -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public' -import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' -import { redirect, error } from '@sveltejs/kit' -import { sequence } from '@sveltejs/kit/hooks' - -async function supabase({ event, resolve }) { - event.locals.supabase = createSupabaseServerClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event, - }) - - /** - * Unlike `supabase.auth.getSession`, which is unsafe on the server because it - * doesn't validate the JWT, this function validates the JWT by first calling - * `getUser` and aborts early if the JWT signature is invalid. - */ - event.locals.safeGetSession = async () => { - const { - data: { user }, - error, - } = await event.locals.supabase.auth.getUser() - if (error) return { session: null, user: null } - - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} - -async function authorization({ event, resolve }) { - // protect requests to all routes that start with /protected-routes - if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') { - const { session } = await event.locals.safeGetSession() - if (!session) { - // the user is not signed in - redirect(303, '/') - } - } - - // protect POST requests to all routes that start with /protected-posts - if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { - const { session } = await event.locals.safeGetSession() - if (!session) { - // the user is not signed in - throw error(303, '/') - } - } - - return resolve(event) -} - -export const handle = sequence(supabase, authorization) -``` - - - - - -```ts src/hooks.server.ts -// src/hooks.server.ts -import { type Handle, redirect, error } from '@sveltejs/kit' -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public' -import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' -import { sequence } from '@sveltejs/kit/hooks' - -async function supabase({ event, resolve }) { - event.locals.supabase = createSupabaseServerClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event, - }) - - /** - * Unlike `supabase.auth.getSession`, which is unsafe on the server because it - * doesn't validate the JWT, this function validates the JWT by first calling - * `getUser` and aborts early if the JWT signature is invalid. - */ - event.locals.safeGetSession = async () => { - const { - data: { user }, - error, - } = await event.locals.supabase.auth.getUser() - if (error) return { session: null, user: null } - - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} - -async function authorization({ event, resolve }) { - // protect requests to all routes that start with /protected-routes - if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') { - const { session } = await event.locals.safeGetSession() - if (!session) { - // the user is not signed in - redirect(303, '/') - } - } - - // protect POST requests to all routes that start with /protected-posts - if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { - const { session } = await event.locals.safeGetSession() - if (!session) { - // the user is not signed in - throw error(303, '/') - } - } - - return resolve(event) -} - -export const handle: Handle = sequence(supabase, authorization) -``` - - - - -## Data fetching - -### Client-side data fetching with RLS - -For [row level security](/docs/guides/database/postgres/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side: - -```svelte src/routes/+page.svelte - - -{#if data.session} -

client-side data fetching with RLS

-
{JSON.stringify(loadedData, null, 2)}
-{/if} -``` - -### Server-side data fetching with RLS - -```svelte src/routes/profile/+page.svelte - - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/profile/+page.ts -// src/routes/profile/+page.ts -import { redirect } from '@sveltejs/kit' - -export const load = async ({ parent }) => { - const { supabase, session } = await parent() - if (!session) { - redirect(303, '/') - } - const { data: tableData } = await supabase.from('test').select('*') - - return { - user: session.user, - tableData, - } -} -``` - -## Saving and deleting the session - -```ts -import { fail, redirect } from '@sveltejs/kit' -import { AuthApiError } from '@supabase/supabase-js' - -export const actions = { - signin: async ({ request, locals: { supabase } }) => { - const formData = await request.formData() - - const email = formData.get('email') as string - const password = formData.get('password') as string - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - - if (error) { - if (error instanceof AuthApiError && error.status === 400) { - return fail(400, { - error: 'Invalid credentials.', - values: { - email, - }, - }) - } - return fail(500, { - error: 'Server error. Try again later.', - values: { - email, - }, - }) - } - - redirect(303, '/dashboard') - }, - - signout: async ({ locals: { supabase } }) => { - await supabase.auth.signOut() - redirect(303, '/') - }, -} -``` - -## Migration guide [#migration] - -### Migrate to 0.10 - -#### PKCE Auth flow - -Proof Key for Code Exchange (PKCE) is the new server-side auth flow implemented by the SvelteKit Auth Helpers. It requires a server endpoint for `/auth/callback` that exchanges an auth `code` for the user's `session`. - -Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/sveltekit#code-exchange-route) above to implement this server endpoint. - -#### Authentication - -For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange route handler - `/auth/callback`. This is an example with the `signUp` function: - -```ts -await supabase.auth.signUp({ - email: 'valid.email@supabase.io', - password: 'sup3rs3cur3', - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, -}) -``` - -### Migrate from 0.8.x to 0.9 [#migration-0-9] - -#### Set up the Supabase client [#migration-set-up-supabase-client] - -In version 0.9 we now setup our Supabase client for the server inside of a `hooks.server.ts` file. - - - - -```js src/lib/db.ts -// src/lib/db.ts -import { createClient } from '@supabase/auth-helpers-sveltekit' -import { env } from '$env/dynamic/public' -// or use the static env - -// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; - -export const supabaseClient = createClient( - env.PUBLIC_SUPABASE_URL, - env.PUBLIC_SUPABASE_PUBLISHABLE_KEY -) -``` - - - - -```js src/hooks.server.ts -// src/hooks.server.ts -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public' -import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' -import type { Handle } from '@sveltejs/kit' - -export const handle: Handle = async ({ event, resolve }) => { - event.locals.supabase = createSupabaseServerClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event, - }) - - /** - * Unlike `supabase.auth.getSession`, which is unsafe on the server because it - * doesn't validate the JWT, this function validates the JWT by first calling - * `getUser` and aborts early if the JWT signature is invalid. - */ - event.locals.safeGetSession = async () => { - const { - data: { user }, - error, - } = await event.locals.supabase.auth.getUser() - if (error) return { session: null, user: null } - - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} -``` - - - - -#### Initialize the client [#migration-initialize-client] - -In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` and create a `+layout.svelte` to handle our event listening for Auth events. - - - - -```svelte src/routes/+layout.svelte - - - - -``` - - - - -```ts src/routes/+layout.ts -// src/routes/+layout.ts -import { invalidate } from '$app/navigation' -import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' -import type { LayoutLoad } from './$types' -import type { Database } from '../DatabaseDefinitions' - -export const load: LayoutLoad = async ({ fetch, data, depends }) => { - depends('supabase:auth') - - const supabase = createSupabaseLoadClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_PUBLISHABLE_KEY, - event: { fetch }, - serverSession: data.session, - }) - - const { - data: { session }, - } = await supabase.auth.getSession() - - return { supabase, session } -} -``` - -```svelte src/routes/+layout.svelte - - - - -``` - - - - -#### Set up hooks [#migration-set-up-hooks] - -Since version 0.9 relies on `hooks.server.ts` to setup our client, we no longer need the `hooks.client.ts` in our project for Supabase related code. - -#### Types [#migration-typings] - - - - -```ts src/app.d.ts -// src/app.d.ts -/// - -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -// and what to do when importing types -declare namespace App { - interface Supabase { - Database: import('./DatabaseDefinitions').Database - SchemaName: 'public' - } - - // interface Locals {} - interface PageData { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - // interface Error {} - // interface Platform {} -} -``` - - - - -```ts src/app.d.ts -// src/app.d.ts -import { SupabaseClient, Session, User } from '@supabase/supabase-js' -import { Database } from './DatabaseDefinitions' - -declare global { - namespace App { - interface Locals { - supabase: SupabaseClient - safeGetSession(): Promise<{ session: Session | null; user: User | null }> - } - interface PageData { - session: Session | null - user: User | null - } - // interface Error {} - // interface Platform {} - } -} -``` - - - - -#### Protecting a page [#migration-protecting-a-page] - - - - -```svelte src/routes/profile/+page.svelte - - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/profile/+page.ts -// src/routes/profile/+page.ts -import type { PageLoad } from './$types' -import { getSupabase } from '@supabase/auth-helpers-sveltekit' -import { redirect } from '@sveltejs/kit' - -export const load: PageLoad = async (event) => { - const { session, supabaseClient } = await getSupabase(event) - if (!session) { - redirect(303, '/') - } - const { data: tableData } = await supabaseClient.from('test').select('*') - - return { - user: session.user, - tableData, - } -} -``` - -
- - -```svelte src/routes/profile/+page.svelte - - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/profile/+page.ts -// src/routes/profile/+page.ts -import type { PageLoad } from './$types' -import { redirect } from '@sveltejs/kit' - -export const load: PageLoad = async ({ parent }) => { - const { supabase, session } = await parent() - if (!session) { - redirect(303, '/') - } - const { data: tableData } = await supabase.from('test').select('*') - - return { - user: session.user, - tableData, - } -} -``` - -
-
- -#### Protecting a API route [#migration-protecting-a-api-route] - - - - -```ts src/routes/api/protected-route/+server.ts -// src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' -import { getSupabase } from '@supabase/auth-helpers-sveltekit' -import { json, redirect } from '@sveltejs/kit' - -export const GET: RequestHandler = async (event) => { - const { session, supabaseClient } = await getSupabase(event) - if (!session) { - redirect(303, '/') - } - const { data } = await supabaseClient.from('test').select('*') - - return json({ data }) -} -``` - - - - -```ts src/routes/api/protected-route/+server.ts -// src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' -import { json, error } from '@sveltejs/kit' - -export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) => { - const { session } = await getSession() - if (!session) { - // the user is not signed in - throw error(401, { message: 'Unauthorized' }) - } - const { data } = await supabase.from('test').select('*') - - return json({ data }) -} -``` - - - - -### Migrate from 0.7.x to 0.8 [#migration-0-8] - -#### Set up the Supabase client [#migration-set-up-supabase-client-0-8] - - - - -```js src/lib/db.ts -import { createClient } from '@supabase/supabase-js' -import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit' -import { dev } from '$app/environment' -import { env } from '$env/dynamic/public' -// or use the static env - -// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; - -export const supabaseClient = createClient( - env.PUBLIC_SUPABASE_URL, - env.PUBLIC_SUPABASE_PUBLISHABLE_KEY, - { - persistSession: false, - autoRefreshToken: false, - } -) - -setupSupabaseHelpers({ - supabaseClient, - cookieOptions: { - secure: !dev, - }, -}) -``` - - - - -```js src/lib/db.ts -import { createClient } from '@supabase/auth-helpers-sveltekit' -import { env } from '$env/dynamic/public' -// or use the static env - -// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; - -export const supabaseClient = createClient( - env.PUBLIC_SUPABASE_URL, - env.PUBLIC_SUPABASE_PUBLISHABLE_KEY -) -``` - - - - -#### Initialize the client [#migration-initialize-client-0-8] - - - - -```svelte src/routes/+layout.svelte - - - -``` - - - - -```svelte src/routes/+layout.svelte - - - -``` - - - - -#### Set up hooks [#migration-set-up-hooks-0-8] - - - - -```ts src/hooks.server.ts -// make sure the supabase instance is initialized on the server -import '$lib/db' -import { dev } from '$app/environment' -import { auth } from '@supabase/auth-helpers-sveltekit/server' - -export const handle = auth() -``` - -**Optional** _if using additional handle methods_ - -```ts src/hooks.server.ts -// make sure the supabase instance is initialized on the server -import '$lib/db' -import { dev } from '$app/environment' -import { auth } from '@supabase/auth-helpers-sveltekit/server' -import { sequence } from '@sveltejs/kit/hooks' - -export const handle = sequence(auth(), yourHandler) -``` - - - - -```ts src/hooks.server.ts -// make sure the supabase instance is initialized on the server -import '$lib/db' -``` - -```ts src/hooks.client.ts -// make sure the supabase instance is initialized on the client -import '$lib/db' -``` - - - - -#### Types [#migration-typings-0-8] - - - - -```ts src/app.d.ts -/// - -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -// and what to do when importing types -declare namespace App { - interface Locals { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - - interface PageData { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - - // interface Error {} - // interface Platform {} -} -``` - - - - -```ts src/app.d.ts -/// - -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -// and what to do when importing types -declare namespace App { - interface Supabase { - Database: import('./DatabaseDefinitions').Database - SchemaName: 'public' - } - - // interface Locals {} - interface PageData { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - // interface Error {} - // interface Platform {} -} -``` - - - - -#### `withPageAuth` [#migration-with-page-auth-0-8] - - - - -```svelte src/routes/protected-route/+page.svelte - - -
Protected content for {user.email}
-

server-side fetched data with RLS:

-
{JSON.stringify(tableData, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/protected-route/+page.ts -import { withAuth } from '@supabase/auth-helpers-sveltekit' -import { redirect } from '@sveltejs/kit' -import type { PageLoad } from './$types' - -export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => { - if (!session.user) { - redirect(303, '/') - } - - const { data: tableData } = await getSupabaseClient().from('test').select('*') - return { tableData, user: session.user } -}) -``` - -
- - -```svelte src/routes/protected-route/+page.svelte - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/protected-route/+page.ts -// src/routes/profile/+page.ts -import type { PageLoad } from './$types' -import { getSupabase } from '@supabase/auth-helpers-sveltekit' -import { redirect } from '@sveltejs/kit' - -export const load: PageLoad = async (event) => { - const { session, supabaseClient } = await getSupabase(event) - if (!session) { - redirect(303, '/') - } - const { data: tableData } = await supabaseClient.from('test').select('*') - - return { - user: session.user, - tableData, - } -} -``` - -
-
- -#### `withApiAuth` [#migration-with-api-auth-0-8] - - - - -```ts src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' -import { withAuth } from '@supabase/auth-helpers-sveltekit' -import { json, redirect } from '@sveltejs/kit' - -interface TestTable { - id: string - created_at: string -} - -export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => { - if (!session.user) { - redirect(303, '/') - } - - const { data } = await getSupabaseClient().from('test').select('*') - - return json({ data }) -}) -``` - - - - -```ts src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' -import { getSupabase } from '@supabase/auth-helpers-sveltekit' -import { json, redirect } from '@sveltejs/kit' - -export const GET: RequestHandler = async (event) => { - const { session, supabaseClient } = await getSupabase(event) - if (!session) { - redirect(303, '/') - } - const { data } = await supabaseClient.from('test').select('*') - - return json({ data }) -} -``` - - - - -### Migrate from 0.6.11 and below to 0.7.0 [#migration-0-7] - -There are numerous breaking changes in the latest 0.7.0 version of this library. - -#### Environment variable prefix - -The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`). - -#### Set up the Supabase client [#migration-set-up-supabase-client-0-7] - - - - -```js src/lib/db.ts -import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit'; - -const { supabaseClient } = createSupabaseClient( - import.meta.env.VITE_SUPABASE_URL as string, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY as string -); - -export { supabaseClient }; -``` - - - - -```js src/lib/db.ts -import { createClient } from '@supabase/supabase-js' -import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit' -import { dev } from '$app/environment' -import { env } from '$env/dynamic/public' -// or use the static env - -// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; - -export const supabaseClient = createClient( - env.PUBLIC_SUPABASE_URL, - env.PUBLIC_SUPABASE_PUBLISHABLE_KEY, - { - persistSession: false, - autoRefreshToken: false, - } -) - -setupSupabaseHelpers({ - supabaseClient, - cookieOptions: { - secure: !dev, - }, -}) -``` - - - - -#### Initialize the client [#migration-initialize-client-0-7] - - - - -```svelte src/routes/__layout.svelte - - - - - -``` - - - - -The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code. - -```svelte src/routes/+layout.svelte - - - -``` - - - - -#### Set up hooks [#migration-set-up-hooks-0-7] - - - - -```ts src/hooks.ts -import { handleAuth } from '@supabase/auth-helpers-sveltekit' -import type { GetSession, Handle } from '@sveltejs/kit' -import { sequence } from '@sveltejs/kit/hooks' - -export const handle: Handle = sequence(...handleAuth()) - -export const getSession: GetSession = async (event) => { - const { user, accessToken, error } = event.locals - return { - user, - accessToken, - error, - } -} -``` - - - - -```ts src/hooks.server.ts -// make sure the supabase instance is initialized on the server -import '$lib/db' -import { dev } from '$app/environment' -import { auth } from '@supabase/auth-helpers-sveltekit/server' - -export const handle = auth() -``` - -**Optional** _if using additional handle methods_ - -```ts src/hooks.server.ts -// make sure the supabase instance is initialized on the server -import '$lib/db' -import { dev } from '$app/environment' -import { auth } from '@supabase/auth-helpers-sveltekit/server' -import { sequence } from '@sveltejs/kit/hooks' - -export const handle = sequence(auth(), yourHandler) -``` - - - - -#### Types [#migration-typings-0-7] - - - - -```ts src/app.d.ts -/// -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare namespace App { - interface UserSession { - user: import('@supabase/supabase-js').User - accessToken?: string - } - - interface Locals extends UserSession { - error: import('@supabase/supabase-js').ApiError - } - - interface Session extends UserSession {} - - // interface Platform {} - // interface Stuff {} -} -``` - - - - -```ts src/app.d.ts -/// - -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -// and what to do when importing types -declare namespace App { - interface Locals { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - - interface PageData { - session: import('@supabase/auth-helpers-sveltekit').SupabaseSession - } - - // interface Error {} - // interface Platform {} -} -``` - - - - -#### Check the user on the client - - - - -```svelte src/routes/index.svelte - - -{#if !$session.user} -

I am not logged in

-{:else} -

Welcome {$session.user.email}

-

I am logged in!

-{/if} -``` - -
- - -```svelte src/routes/+page.svelte - - -{#if !$page.data.session.user} -

I am not logged in

-{:else} -

Welcome {$page.data.session.user.email}

-

I am logged in!

-{/if} -``` - -
-
- -#### `withPageAuth` - - - - -```svelte src/routes/protected-route.svelte - - - - -
Protected content for {user.email}
-

server-side fetched data with RLS:

-
{JSON.stringify(data, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
-``` - -
- - -```svelte src/routes/protected-route/+page.svelte - - -
Protected content for {user.email}
-

server-side fetched data with RLS:

-
{JSON.stringify(tableData, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
-``` - -```ts src/routes/protected-route/+page.ts -import { withAuth } from '@supabase/auth-helpers-sveltekit' -import { redirect } from '@sveltejs/kit' -import type { PageLoad } from './$types' - -export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => { - if (!session.user) { - redirect(303, '/') - } - - const { data: tableData } = await getSupabaseClient().from('test').select('*') - return { tableData, user: session.user } -}) -``` - -
-
- -#### `withApiAuth` - - - - -```ts src/routes/api/protected-route.ts -import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit' -import type { RequestHandler } from './__types/protected-route' - -interface TestTable { - id: string - created_at: string -} - -interface GetOutput { - data: TestTable[] -} - -export const GET: RequestHandler = async ({ locals, request }) => - withApiAuth({ user: locals.user }, async () => { - // Run queries with RLS on the server - const { data } = await supabaseServerClient(request).from('test').select('*') - - return { - status: 200, - body: { data }, - } - }) -``` - - - - -```ts src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types'; -import { withAuth } from '@supabase/auth-helpers-sveltekit'; -import { json, redirect } from '@sveltejs/kit'; - -interface TestTable { - id: string; - created_at: string; -} - -export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => { - if (!session.user) { - redirect(303, '/'); - } - - const { data } = await getSupabaseClient() - .from('test') - .select('*'); - - return json({ data }); -); -``` - - - - -## Additional links - -- [Auth Helpers Source code](https://github.com/supabase/auth-helpers) -- [SvelteKit example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit) - -
- -
diff --git a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx index 5fb8701653eef..fde1411b01eff 100644 --- a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx @@ -1,31 +1,36 @@ --- id: 'auth-twitter' -title: 'Login with Twitter' -description: 'Add Twitter OAuth to your Supabase project' +title: 'Login with X / Twitter' +description: 'Add X / Twitter OAuth to your Supabase project' --- -To enable Twitter Auth for your project, you need to set up a Twitter OAuth application and add the application credentials in the Supabase Dashboard. +To enable X / Twitter Auth for your project, you need to set up an X OAuth 2.0 application and add the application credentials in the Supabase Dashboard. ## Overview -Setting up Twitter logins for your application consists of 3 parts: + -- Create and configure a Twitter Project and App on the [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard). -- Add your Twitter `API Key` and `API Secret Key` to your [Supabase Project](/dashboard). +We recommend using the **X / Twitter (OAuth 2.0)** provider. The legacy **Twitter (OAuth 1.0a)** +provider will be deprecated in future releases. + + + +Setting up X / Twitter logins for your application consists of 3 parts: + +- Create and configure an X Project and App on the [X Developer Dashboard](https://developer.x.com/en/portal/dashboard). +- Add your X `API Key` and `API Secret Key` to your [Supabase Project](/dashboard). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Access your Twitter Developer account +## Access your X developer account -- Go to [developer.twitter.com](https://developer.twitter.com). +- Go to [developer.x.com](https://developer.x.com). - Click on `Sign in` at the top right to log in. -![Twitter Developer Portal.](/docs/img/guides/auth-twitter/twitter-portal.png) - ## Find your callback URL -<$Partial path="social_provider_setup.mdx" variables={{ "provider": "Twitter" }} /> +<$Partial path="social_provider_setup.mdx" variables={{ "provider": "X / Twitter (OAuth 2.0)" }} /> -## Create a Twitter OAuth app +## Create an X OAuth app - Click `+ Create Project`. - Enter your project name, click `Next`. @@ -46,25 +51,25 @@ Setting up Twitter logins for your application consists of 3 parts: - Enter your `Privacy policy URL`. - Click `Save`. -## Enter your Twitter credentials into your Supabase project +## Enter your X credentials into your Supabase project -<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Twitter" }} /> +<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "X / Twitter (OAuth 2.0)" }} /> -You can also configure the Twitter auth provider using the Management API: +You can also configure the X / Twitter (OAuth 2.0) auth provider using the Management API: ```bash # Get your access token from https://supabase.com/dashboard/account/tokens export SUPABASE_ACCESS_TOKEN="your-access-token" export PROJECT_REF="your-project-ref" -# Configure Twitter auth provider +# Configure X / Twitter (OAuth 2.0) auth provider curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ - "external_twitter_enabled": true, - "external_twitter_client_id": "your-twitter-api-key", - "external_twitter_secret": "your-twitter-api-secret-key" + "external_x_enabled": true, + "external_x_client_id": "your-x-api-key", + "external_x_secret": "your-x-api-secret-key" }' ``` @@ -81,7 +86,7 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ <$Partial path="create_client_snippet.mdx" /> -When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `twitter` as the `provider`: +When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `x` as the `provider`: ```js import { createClient } from '@supabase/supabase-js' @@ -91,9 +96,9 @@ const supabase = createClient( ) // ---cut--- -async function signInWithTwitter() { +async function signInWithX() { const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'twitter', + provider: 'x', }) } ``` @@ -101,12 +106,12 @@ async function signInWithTwitter() { -When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `twitter` as the `provider`: +When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `x` as the `provider`: ```dart -Future signInWithTwitter() async { +Future signInWithX() async { await supabase.auth.signInWithOAuth( - OAuthProvider.twitter, + OAuthProvider.x, redirectTo: kIsWeb ? null : 'my.scheme://my-host', // Optionally set the redirect link to bring back the user via deeplink. authScreenLaunchMode: kIsWeb ? LaunchMode.platformDefault : LaunchMode.externalApplication, // Launch the auth screen in a new webview on mobile. @@ -118,11 +123,11 @@ Future signInWithTwitter() async { <$Show if="sdk:kotlin"> -When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Twitter` as the `Provider`: +When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `X` as the `Provider`: ```kotlin -suspend fun signInWithTwitter() { - supabase.auth.signInWith(Twitter) +suspend fun signInWithX() { + supabase.auth.signInWith(X) } ``` @@ -187,4 +192,4 @@ suspend fun signOut() { - [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) -- [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard) +- [X Developer Dashboard](https://developer.x.com/en/portal/dashboard) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index f69cd73633c6a..8090b30ff31e2 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -161,6 +161,7 @@ Sam Rose Sean Oliver Sean Thompson Sergio Cioban Filho +Shane E Sreyas Udayavarman Stanislav M Stephen Morgan diff --git a/apps/docs/styles/main.scss b/apps/docs/styles/main.scss index c82c2eb688c99..a581a22e6cd24 100644 --- a/apps/docs/styles/main.scss +++ b/apps/docs/styles/main.scss @@ -310,39 +310,6 @@ th code { image-rendering: high-quality; } -/* Loaders */ - -.shimmering-loader { - animation: shimmer 2s infinite linear; - background: linear-gradient( - to right, - hsl(var(--border-default)) 4%, - hsl(var(--background-surface-200)) 25%, - hsl(var(--border-default)) 36% - ); - background-size: 1000px 100%; -} - -.dark .shimmering-loader { - animation: shimmer 2s infinite linear; - background: linear-gradient( - to right, - hsl(var(--border-default)) 4%, - hsl(var(--border-control)) 25%, - hsl(var(--border-default)) 36% - ); - background-size: 1000px 100%; -} - -@keyframes shimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - .skip-link { @apply sr-only; } diff --git a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx index 0de46445319fc..44c420479f3ef 100644 --- a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx +++ b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx @@ -5,12 +5,12 @@ import type { RenderCellProps } from 'react-data-grid' import { convertByteaToHex } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { useTableQuery } from 'data/tables/table-retrieve-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import type { SupaRow } from '../../types' import { NullValue } from '../common/NullValue' import { ReferenceRecordPeek } from './ReferenceRecordPeek' diff --git a/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx b/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx index 26eb6ac3f0492..0ac4073ff3a4d 100644 --- a/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx +++ b/apps/studio/components/grid/components/formatter/ReferenceRecordPeek.tsx @@ -10,11 +10,11 @@ import { getColumnDefaultWidth, } from 'components/grid/utils/gridColumns' import { convertByteaToHex } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface ReferenceRecordPeekProps { table: PostgresTable diff --git a/apps/studio/components/interfaces/APIKeys/CreateNewAPIKeysButton.tsx b/apps/studio/components/interfaces/APIKeys/CreateNewAPIKeysButton.tsx index a1262b200a8db..16456e7045b05 100644 --- a/apps/studio/components/interfaces/APIKeys/CreateNewAPIKeysButton.tsx +++ b/apps/studio/components/interfaces/APIKeys/CreateNewAPIKeysButton.tsx @@ -5,6 +5,7 @@ import { useParams } from 'common' import { useAPIKeyCreateMutation } from 'data/api-keys/api-key-create-mutation' import { AlertDialog, + AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, @@ -49,16 +50,16 @@ export const CreateNewAPIKeysButton = () => { Create new API keys - This will create a default publishable key and a default secret key named{' '} + This will create a default publishable key and a default secret key both named{' '} default. These keys are required to connect your application to your Supabase project. Cancel - + diff --git a/apps/studio/components/interfaces/Account/AuditLogs.tsx b/apps/studio/components/interfaces/Account/AuditLogs.tsx index e5ff6d9fbe56f..7dee611eaf88d 100644 --- a/apps/studio/components/interfaces/Account/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Account/AuditLogs.tsx @@ -1,21 +1,21 @@ +import { keepPreviousData } from '@tanstack/react-query' +import { useDebounce } from '@uidotdev/usehooks' import dayjs from 'dayjs' import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' -import { keepPreviousData } from '@tanstack/react-query' -import { useDebounce } from '@uidotdev/usehooks' import { LogDetailsPanel } from 'components/interfaces/AuditLogs/LogDetailsPanel' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import type { AuditLog } from 'data/organizations/organization-audit-logs-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProfileAuditLogsQuery } from 'data/profile/profile-audit-logs-query' import { useProjectsInfiniteQuery } from 'data/projects/projects-infinite-query' import { Button } from 'ui' -import { TimestampInfo } from 'ui-patterns' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { LogsDatePicker } from '../Settings/Logs/Logs.DatePickers' export const AuditLogs = () => { diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx index 339521850b27b..e941faeb1e066 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx @@ -28,7 +28,7 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export const AccountConnections = () => { const { diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx index 08ebeb932bd68..edd200577cab9 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx @@ -25,12 +25,6 @@ import { TooltipTrigger, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -import { - ChangeEmailAddressForm, - GitHubChangeEmailAddress, - SSOChangeEmailAddress, -} from './ChangeEmailAddress' import { PageSection, PageSectionContent, @@ -39,6 +33,12 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { + ChangeEmailAddressForm, + GitHubChangeEmailAddress, + SSOChangeEmailAddress, +} from './ChangeEmailAddress' const getProviderName = (provider: string) => provider === 'github' diff --git a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx index adb4fe08a2b71..b5b3b9c7520b3 100644 --- a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx @@ -69,7 +69,12 @@ export const ProfileInformation = () => { onSuccess: (data) => { toast.success('Successfully saved profile') const { first_name, last_name, username, primary_email } = data - form.reset({ first_name, last_name, username, primary_email }) + form.reset({ + first_name: first_name ?? undefined, + last_name: last_name ?? undefined, + username, + primary_email, + }) }, onError: (error) => toast.error(`Failed to update profile: ${error.message}`), }) diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx index 28956b51802a5..d72c035224ca4 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -4,7 +4,6 @@ import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS } from 'common' import InformationBox from 'components/ui/InformationBox' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { organizationKeys } from 'data/organizations/keys' import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation' import { useMfaEnrollMutation } from 'data/profile/mfa-enroll-mutation' @@ -12,6 +11,7 @@ import { useMfaUnenrollMutation } from 'data/profile/mfa-unenroll-mutation' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { Input } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' interface AddNewFactorModalProps { visible: boolean diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx index a8fecf483ffdc..45e7839730c70 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx @@ -3,10 +3,10 @@ import { AlertCircle } from 'lucide-react' import { useState } from 'react' import AlertError from 'components/ui/AlertError' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query' import { DATETIME_FORMAT } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AddNewFactorModal } from './AddNewFactorModal' import DeleteFactorModal from './DeleteFactorModal' diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx index e1ff48a069cb7..f796154f6da59 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx @@ -16,6 +16,7 @@ import { } from 'ui-patterns/CommandMenu' import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' +import { toast } from 'sonner' const API_KEYS_PAGE_NAME = 'API Keys' @@ -46,7 +47,9 @@ export function useApiKeysCommands() { id: 'publishable-key', name: `Copy publishable key`, action: () => { - copyToClipboard(publishableKey.api_key ?? '') + copyToClipboard(publishableKey.api_key ?? '', () => { + toast.success('Publishable key copied to clipboard') + }) setIsOpen(false) }, badge: () => ( @@ -62,7 +65,9 @@ export function useApiKeysCommands() { id: key.id, name: `Copy secret key (${key.name})`, action: () => { - copyToClipboard(key.api_key ?? '') + copyToClipboard(key.api_key ?? '', () => { + toast.success('Secret key copied to clipboard') + }) setIsOpen(false) }, badge: () => ( @@ -79,7 +84,9 @@ export function useApiKeysCommands() { id: 'anon-key', name: `Copy anonymous API key`, action: () => { - copyToClipboard(anonKey.api_key ?? '') + copyToClipboard(anonKey.api_key ?? '', () => { + toast.success('Anonymous API key copied to clipboard') + }) setIsOpen(false) }, badge: () => ( @@ -96,7 +103,9 @@ export function useApiKeysCommands() { id: 'service-key', name: `Copy service API key`, action: () => { - copyToClipboard(serviceKey.api_key ?? '') + copyToClipboard(serviceKey.api_key ?? '', () => { + toast.success('Service key copied to clipboard') + }) setIsOpen(false) }, badge: () => ( diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx index f84d8c6bb41bc..b29bfc5415b2d 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiUrl.tsx @@ -6,6 +6,7 @@ import { Badge, copyToClipboard } from 'ui' import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' +import { toast } from 'sonner' export function useApiUrlCommand() { const setIsOpen = useSetCommandMenuOpen() @@ -27,7 +28,9 @@ export function useApiUrlCommand() { id: 'api-url', name: 'Copy API URL', action: () => { - copyToClipboard(apiUrl ?? '') + copyToClipboard(apiUrl ?? '', () => { + toast.success('API URL copied to clipboard') + }) setIsOpen(false) }, icon: () => , diff --git a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx index 0ca0e48449a30..68d7992f03617 100644 --- a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx @@ -38,7 +38,7 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { NO_REQUIRED_CHARACTERS } from './Auth.constants' const schema = object({ diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index 9c77cd7c6662d..0f8a3952ba7df 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -10,7 +10,6 @@ import { z } from 'zod' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -38,6 +37,7 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { TEMPLATES_SCHEMAS } from '../AuthTemplatesValidation' import { slugifyTitle } from './EmailTemplates.utils' diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index 0fce4f17a4a4e..7a0fc73afa89c 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -9,7 +9,6 @@ import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' @@ -35,6 +34,7 @@ import { } from 'ui' import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { CreateOrUpdateOAuthAppSheet } from './CreateOrUpdateOAuthAppSheet' import { DeleteOAuthAppModal } from './DeleteOAuthAppModal' diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx index bdbf02ccfab2e..6a46e090259a0 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx @@ -9,7 +9,6 @@ import * as z from 'zod' import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query' @@ -29,6 +28,7 @@ import { import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const configUrlSchema = z.object({ id: z.string(), diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx index f28b86942649c..0eaaa9ede661b 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx @@ -1,11 +1,11 @@ import { SYSTEM_ROLES } from 'components/interfaces/Database/Roles/Roles.constants' import AlertError from 'components/ui/AlertError' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { sortBy } from 'lodash' import MultiSelect from 'ui-patterns/MultiSelectDeprecated' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface PolicyRolesProps { selectedRoles: string[] diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 585dbb3cf97b1..02c34be3d6310 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -19,7 +19,7 @@ import { TableRow, } from 'ui' import { Admonition } from 'ui-patterns' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { usePoliciesData } from '../PoliciesDataContext' import { PolicyRow } from './PolicyRow' import type { PolicyTable } from './PolicyTableRow.types' diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 2f7497d8b73ef..0e5899e7224e7 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -9,7 +9,6 @@ import * as z from 'zod' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -29,6 +28,7 @@ import { } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils' export const RateLimits = () => { diff --git a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx index 5dc5396ae0dac..c204d91b81f6c 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx @@ -6,12 +6,12 @@ import { useEffect } from 'react' import { useParams } from 'common' import { LOGS_TABLES } from 'components/interfaces/Settings/Logs/Logs.constants' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { User } from 'data/auth/users-infinite-query' import useLogsPreview from 'hooks/analytics/useLogsPreview' import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState' import { Button, cn, CriticalIcon, Separator } from 'ui' import { Admonition, TimestampInfo } from 'ui-patterns' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { UserHeader } from './UserHeader' import { PANEL_PADDING } from './Users.constants' diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx index 91a86aa1c2a76..ae56cca6877e9 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx @@ -14,7 +14,7 @@ import { isEqual } from 'lodash' import { useRef, useState } from 'react' import { toast } from 'sonner' import { Button, Checkbox_Shadcn_, Label_Shadcn_, Modal } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface AddPaymentMethodFormProps { returnUrl: string diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx index f7044cc6f80ae..99eda56f6a1b2 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx @@ -4,11 +4,11 @@ import Link from 'next/link' import { useParams } from 'common' import { SupportLink } from 'components/interfaces/Support/SupportLink' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import CreditCard from './CreditCard' const CurrentPaymentMethod = () => { diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx index 10efc0bc373a3..5b1f0194104fe 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx @@ -16,7 +16,6 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' import NoPermission from 'components/ui/NoPermission' import PartnerManagedResource from 'components/ui/PartnerManagedResource' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -25,6 +24,7 @@ import { MANAGED_BY } from 'lib/constants/infrastructure' import { getURL } from 'lib/helpers' import { Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import ChangePaymentMethodModal from './ChangePaymentMethodModal' import CreditCard from './CreditCard' import DeletePaymentMethodModal from './DeletePaymentMethodModal' diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx index 629739d8ef42b..f1a67a24999d1 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx @@ -4,10 +4,10 @@ import { useRouter } from 'next/router' import { PropsWithChildren, ReactNode } from 'react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import type { Branch } from 'data/branches/branches-query' import Link from 'next/link' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { WorkflowLogs } from './WorkflowLogs' interface BranchManagementSectionProps { diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 92ba49c397c10..749a3826ee86a 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -16,7 +16,6 @@ import { BranchingPITRNotice } from 'components/layouts/AppLayout/EnableBranchin import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UpgradeToPro } from 'components/ui/UpgradeToPro' import { useBranchCreateMutation } from 'data/branches/branch-create-mutation' import { useBranchesQuery } from 'data/branches/branches-query' @@ -55,7 +54,7 @@ import { cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { estimateComputeSize, estimateDiskCost, diff --git a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx index 91a6c2b6d0373..b71554c2a4a00 100644 --- a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx @@ -10,7 +10,6 @@ import * as z from 'zod' import { useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import AlertError from 'components/ui/AlertError' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation' import { Branch, useBranchesQuery } from 'data/branches/branches-query' import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query' @@ -38,6 +37,7 @@ import { cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' interface EditBranchModalProps { branch?: Branch diff --git a/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx b/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx index 1c86de5f88195..717875cf34875 100644 --- a/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx +++ b/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx @@ -109,7 +109,7 @@ export const OutOfDateNotice = ({ - Update branch with modified functions? + Update branch with modified functions This branch has {modifiedFunctionsCount} modified edge function {modifiedFunctionsCount !== 1 ? 's' : ''} that will be overwritten when updating @@ -119,7 +119,7 @@ export const OutOfDateNotice = ({ Cancel - handleUpdate(true)}> + handleUpdate(true)}> Update anyway diff --git a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx index 59dce1062d612..954ad513a7711 100644 --- a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx @@ -2,7 +2,6 @@ import dynamic from 'next/dynamic' import { forwardRef, HTMLAttributes, useMemo } from 'react' import { useParams } from 'common' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query' @@ -11,6 +10,7 @@ import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization import { pluckObjectFields } from 'lib/helpers' import { useTrack } from 'lib/telemetry/track' import { cn, CopyCallbackContext } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { getAddons } from '../Billing/Subscription/Subscription.utils' import type { projectKeys } from './Connect.types' import { getConnectionStrings } from './DatabaseSettings.utils' diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 608de62205c63..130a129a3df98 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -7,7 +7,6 @@ import { getAddons } from 'components/interfaces/Billing/Subscription/Subscripti import AlertError from 'components/ui/AlertError' import { DatabaseSelector } from 'components/ui/DatabaseSelector' import { InlineLink } from 'components/ui/InlineLink' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' @@ -33,6 +32,7 @@ import { Separator, cn, } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { CONNECTION_PARAMETERS, type ConnectionStringMethod, diff --git a/apps/studio/components/interfaces/Connect/McpTabContent.tsx b/apps/studio/components/interfaces/Connect/McpTabContent.tsx index ea33579fbbe50..b41b54e03c276 100644 --- a/apps/studio/components/interfaces/Connect/McpTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/McpTabContent.tsx @@ -1,11 +1,11 @@ import { IS_PLATFORM, useParams } from 'common' import Panel from 'components/ui/Panel' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { BASE_PATH } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' import { useTheme } from 'next-themes' import { useMemo, useState } from 'react' import { createMcpCopyHandler, McpConfigPanel, type McpClient } from 'ui-patterns/McpUrlBuilder' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import type { projectKeys } from './Connect.types' export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => { diff --git a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx index 96421f09e2bec..1caaf86a565e1 100644 --- a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -49,7 +49,6 @@ export const BackupsList = () => { }, }) - const planKey = backups?.tierKey ?? '' const sortedBackups = (backups?.backups ?? []).sort( (a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf() ) @@ -74,7 +73,7 @@ export const BackupsList = () => { return ( <>
- {sortedBackups.length === 0 && planKey !== 'FREE' ? ( + {sortedBackups.length === 0 ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx index 48fb9810bb277..6c4243e6e7569 100644 --- a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx +++ b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx @@ -6,7 +6,6 @@ import { toast } from 'sonner' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import SchemaSelector from 'components/ui/SchemaSelector' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query' import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' @@ -27,6 +26,7 @@ import { TableHeader, TableRow, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel' import DeleteEnumeratedTypeModal from './DeleteEnumeratedTypeModal' diff --git a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index d0566a457e0d0..5aafee8260e8f 100644 --- a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' import { DocsButton } from 'components/ui/DocsButton' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation' import { useSchemasQuery } from 'data/database/schemas-query' import { executeSql } from 'data/sql/execute-sql-query' @@ -23,6 +22,7 @@ import { WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' const orioleExtCallOuts = ['vector', 'postgis'] diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCardSkeleton.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCardSkeleton.tsx index dff90b7e6567b..85c57d7d6f52c 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCardSkeleton.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCardSkeleton.tsx @@ -1,5 +1,5 @@ -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { Toggle } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export interface ExtensionCardSkeletonProps { index?: number diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx index 06bedcf8e5e87..f4dfd26a69e74 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx @@ -6,7 +6,6 @@ import { useEffect, useState } from 'react' import { useParams } from 'common' import InformationBox from 'components/ui/InformationBox' import { NoSearchResults } from 'components/ui/NoSearchResults' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -21,6 +20,7 @@ import { TableHeader, TableRow, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ExtensionRow } from './ExtensionRow' import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants' diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 067be68ca6c81..57c7a40814924 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -15,7 +15,6 @@ import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import SchemaSelector from 'components/ui/SchemaSelector' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseFunctionDeleteMutation } from 'data/database-functions/database-functions-delete-mutation' import type { DatabaseFunction } from 'data/database-functions/database-functions-query' import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query' @@ -37,6 +36,7 @@ import { TableHeader, TableRow, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning' import FunctionList from './FunctionList' diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx index 64e0d1b47b4a5..e87905d435aec 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx @@ -8,13 +8,13 @@ import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' import { NoSearchResults } from 'components/ui/NoSearchResults' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' import { noop } from 'lib/void' import { Input } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { HooksListEmpty } from './HooksListEmpty' import { SchemaTable } from './SchemaTable' diff --git a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 224c87618d337..6db649328cf52 100644 --- a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -5,7 +5,6 @@ import { toast } from 'sonner' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import { DocsButton } from 'components/ui/DocsButton' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useDatabaseIndexCreateMutation } from 'data/database-indexes/index-create-mutation' import { useSchemasQuery } from 'data/database/schemas-query' import { useTableColumnsQuery } from 'data/database/table-columns-query' @@ -36,6 +35,7 @@ import { import { Admonition } from 'ui-patterns' import { MultiSelectOption } from 'ui-patterns/MultiSelectDeprecated' import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { INDEX_TYPES } from './Indexes.constants' diff --git a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx index a37cc3c922f26..e463dcb481140 100644 --- a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -8,7 +8,6 @@ import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import SchemaSelector from 'components/ui/SchemaSelector' -import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseIndexDeleteMutation } from 'data/database-indexes/index-delete-mutation' import { useIndexesQuery, type DatabaseIndex } from 'data/database-indexes/indexes-query' import { useSchemasQuery } from 'data/database/schemas-query' @@ -29,6 +28,7 @@ import { } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' +import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import CreateIndexSidePanel from './CreateIndexSidePanel' diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx index 14b7e6062d973..d8b1bd5a7a0d9 100644 --- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -5,7 +5,6 @@ import { SupportCategories } from '@supabase/shared-types/out/constants' import { SupportLink } from 'components/interfaces/Support/SupportLink' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import { InlineLink } from 'components/ui/InlineLink' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' @@ -27,6 +26,7 @@ import { } from 'ui' import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { MigrationsEmptyState } from './MigrationsEmptyState' const Migrations = () => { diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx index 70def772d885d..853e575b6a5bc 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx @@ -1,5 +1,5 @@ -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { Switch, TableCell, TableRow } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export interface PublicationSkeletonProps { index?: number diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/AdvancedSettings.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/AdvancedSettings.tsx index 47ab32593b554..11dc657b87605 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/AdvancedSettings.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/AdvancedSettings.tsx @@ -35,7 +35,7 @@ export const AdvancedSettings = ({ form }: { form: UseFormReturn
- + {/* Batch wait time - applies to all destinations */} ( @@ -61,34 +61,32 @@ export const AdvancedSettings = ({ form }: { form: UseFormReturn - ( - - Maximum staleness (minutes) - BigQuery only -
- } - layout="vertical" - description="Maximum age of cached data before BigQuery reads from base tables at query time. Lower values ensure fresher results but may increase query costs." - > - - - - - )} - /> -
+ ( + + Maximum staleness (minutes) + BigQuery only +
+ } + layout="horizontal" + description="Maximum age of cached data before BigQuery reads from base tables at query time. Lower values ensure fresher results but may increase query costs." + > + + + + + )} + /> )} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationNameInput.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationNameInput.tsx index 3e9208b371b59..d444f9ae62457 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationNameInput.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationNameInput.tsx @@ -14,11 +14,7 @@ export const DestinationNameInput = ({ form }: DestinationNameInputProps) => { control={form.control} name="name" render={({ field }) => ( - + diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index 1bc865359a442..69ad59bb8c874 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -7,6 +7,7 @@ import { toast } from 'sonner' import * as z from 'zod' import { useFlag, useParams } from 'common' +import { CreateAnalyticsBucketSheet } from 'components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPrimaryKeysExists } from 'data/database/primary-keys-exists-query' @@ -27,6 +28,7 @@ import { } from 'state/replication-pipeline-request-status' import { Button, + cn, DialogSectionSeparator, Form_Shadcn_, Sheet, @@ -91,6 +93,7 @@ export const DestinationPanel = ({ const editMode = !!existingDestination const [showDisclaimerDialog, setShowDisclaimerDialog] = useState(false) const [publicationPanelVisible, setPublicationPanelVisible] = useState(false) + const [newBucketSheetVisible, setNewBucketSheetVisible] = useState(false) const [pendingFormValues, setPendingFormValues] = useState | null>( null ) @@ -438,9 +441,12 @@ export const DestinationPanel = ({ <>
@@ -458,13 +464,12 @@ export const DestinationPanel = ({ ) : (
-
-

- Destination details -

-

- Name your destination and choose which data to replicate -

+ + + + +
+

Destination details

@@ -476,23 +481,21 @@ export const DestinationPanel = ({ />
+ - + {selectedType === 'BigQuery' && etlEnableBigQuery ? ( - <> - - - + ) : selectedType === 'Analytics Bucket' && etlEnableIceberg ? ( - <> - - - + setNewBucketSheetVisible(true)} + /> ) : null} + + @@ -526,6 +529,11 @@ export const DestinationPanel = ({ onClose={() => setPublicationPanelVisible(false)} /> + + }) => { return ( - <> -
-

BigQuery settings

-

- Configure how data is sent to your BigQuery destination -

-
-
+
+

BigQuery settings

+
( ( @@ -79,7 +74,7 @@ export const BigQueryFields = ({ form }: { form: UseFormReturn (
- +
) } @@ -113,9 +108,11 @@ export const BigQueryFields = ({ form }: { form: UseFormReturn setIsFormInteracting: (value: boolean) => void + onSelectNewBucket: () => void }) => { const { warehouseName, type, s3AccessKeyId, namespace } = form.watch() const [showCatalogToken, setShowCatalogToken] = useState(false) @@ -166,21 +163,17 @@ export const AnalyticsBucketFields = ({ ) return ( - <> -
-

Analytics Bucket settings

-

- Configure how data is sent to your Analytics Bucket destination -

-
-
+
+

Analytics Bucket settings

+ +
( @@ -209,10 +202,14 @@ export const AnalyticsBucketFields = ({ { - setIsFormInteracting(true) - field.onChange(value) - // [Joshen] Ideally should select the first namespace of the selected bucket - form.setValue('namespace', '') + if (value === 'new-bucket') { + onSelectNewBucket() + } else { + setIsFormInteracting(true) + field.onChange(value) + // [Joshen] Ideally should select the first namespace of the selected bucket + form.setValue('namespace', '') + } }} > @@ -231,6 +228,10 @@ export const AnalyticsBucketFields = ({ )) )} + + + Create a new bucket + @@ -246,7 +247,7 @@ export const AnalyticsBucketFields = ({ render={({ field }) => ( @@ -300,7 +301,7 @@ export const AnalyticsBucketFields = ({ )) )} - {namespaces.length > 0 && } + Create a new namespace @@ -320,7 +321,7 @@ export const AnalyticsBucketFields = ({ render={({ field }) => ( @@ -337,7 +338,7 @@ export const AnalyticsBucketFields = ({ name="catalogToken" render={({ field }) => ( ( - + +

+ Access keys are managed in your Storage{' '} + S3 settings +

+ + {isSuccessKeys && keyNoLongerExists && ( + +

+ Please select another key or create a new set, as this destination will not + work otherwise. S3 access keys can be managed in your{' '} + + storage settings + +

+
+ )} + + {s3AccessKeyId === CREATE_NEW_KEY && ( + + )} +
+ } + className="px-5" + > {isLoadingKeys ? (
- +
) } diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index d99fb5fc99d0b..c7b9362a71761 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -1,12 +1,15 @@ import type { UseFormReturn } from 'react-hook-form' import { useFlag } from 'common' +import { AnalyticsBucket, BigQuery } from 'icons' import { + cn, FormControl_Shadcn_, FormField_Shadcn_, RadioGroupStacked, RadioGroupStackedItem, } from 'ui' +import { Admonition } from 'ui-patterns' import type { DestinationPanelSchemaType } from './DestinationPanel.schema' type DestinationTypeSelectionProps = { @@ -20,16 +23,15 @@ export const DestinationTypeSelection = ({ form, editMode }: DestinationTypeSele return (
-

Destination type

- {editMode ? ( -

- The destination type cannot be changed after creation -

- ) : ( -

- Choose which platform to send your database changes to -

- )} +
+

Type

+ {editMode && ( + + )} +
field.onChange(value)} + className={cn( + 'grid grid-cols-2 [&>button>div]:py-4', + '[&>button:first-of-type]:rounded-none [&>button:last-of-type]:rounded-none', + '[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg' + )} > {((!editMode && etlEnableBigQuery) || (editMode && field.value === 'BigQuery')) && ( + > +
+ +
+

BigQuery

+

+ Send data to Google Cloud's data warehouse for analytics and business + intelligence +

+
+
+
)} {((!editMode && etlEnableIceberg) || (editMode && field.value === 'Analytics Bucket')) && ( + value="Analytics Bucket" + > +
+ +
+

Analytics Bucket

+

+ Send data to Apache Iceberg tables in your Supabase Storage for flexible + analytics workflows +

+
+
+
)} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/PublicationSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/PublicationSelection.tsx index 7b619107dbb9d..0c3da86f327f5 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/PublicationSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/PublicationSelection.tsx @@ -51,56 +51,54 @@ export const PublicationSelection = ({ const hasTablesWithNoPrimaryKeys = (checkPrimaryKeysExistsData?.offendingTables ?? []).length > 0 return ( - <> - ( - - - onSelectNewPublication()} - /> - - {isSelectedPublicationMissing ? ( - -

- The publication {publicationName} was - not found, it may have been renamed or deleted, please select another one. -

-
- ) : hasTablesWithNoPrimaryKeys ? ( - -

- Replication requires every table in the publication to have a primary key to work, - which these tables are missing: -

-
    - {(checkPrimaryKeysExistsData?.offendingTables ?? []).map((x) => { - const value = `${x.schema}.${x.name}` - return ( -
  • - - {value} - -
  • - ) - })} -
-

Ensure that these tables have primary keys first.

-
- ) : null} -
- )} - /> - + ( + + + onSelectNewPublication()} + /> + + {isSelectedPublicationMissing ? ( + +

+ The publication {publicationName} was + not found, it may have been renamed or deleted, please select another one. +

+
+ ) : hasTablesWithNoPrimaryKeys ? ( + +

+ Replication requires every table in the publication to have a primary key to work, + which these tables are missing: +

+
    + {(checkPrimaryKeysExistsData?.offendingTables ?? []).map((x) => { + const value = `${x.schema}.${x.name}` + return ( +
  • + + {value} + +
  • + ) + })} +
+

Ensure that these tables have primary keys first.

+
+ ) : null} +
+ )} + /> ) } diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index a42291ccd93d7..5e818630956f9 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -18,7 +18,7 @@ import { } from 'state/replication-pipeline-request-status' import type { ResponseError } from 'types' import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteDestination } from './DeleteDestination' import { DestinationPanel } from './DestinationPanel/DestinationPanel' import { DestinationPanelSchemaType } from './DestinationPanel/DestinationPanel.schema' diff --git a/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx index 12fa405e96782..e4e48da7146bd 100644 --- a/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { X } from 'lucide-react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' @@ -9,13 +8,11 @@ import { useCreatePublicationMutation } from 'data/replication/publication-creat import { useReplicationTablesQuery } from 'data/replication/tables-query' import { Button, - cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Sheet, - SheetClose, SheetContent, SheetDescription, SheetFooter, @@ -80,27 +77,11 @@ export const NewPublicationPanel = ({ visible, sourceId, onClose }: NewPublicati return ( <> - +
-
-
- New Publication - - Create a new publication to replicate table changes to destinations - -
- - - Close - -
+ Create a new Publication + Replicate table changes to destinations
@@ -134,9 +115,11 @@ export const NewPublicationPanel = ({ visible, sourceId, onClose }: NewPublicati onValuesChange={field.onChange} disabled={creatingPublication} > - - - + {tables?.map((table) => ( diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx index 6d456e5a3b58e..27dc8ba81e68f 100644 --- a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx @@ -7,7 +7,7 @@ import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status- import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' import type { ResponseError } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { getPipelineStateMessages } from './Pipeline.utils' import { PipelineStatusName } from './Replication.constants' diff --git a/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx b/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx index f577f9377233d..0e021a1d6b1e0 100644 --- a/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx +++ b/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx @@ -78,10 +78,11 @@ export const PublicationsComboBox = ({ {selectedPublication || 'Select publication'} - + diff --git a/apps/studio/components/interfaces/Database/Replication/ResetTableButton.tsx b/apps/studio/components/interfaces/Database/Replication/ResetTableButton.tsx index dcec5c8f44a90..6c399ae2bf469 100644 --- a/apps/studio/components/interfaces/Database/Replication/ResetTableButton.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ResetTableButton.tsx @@ -61,34 +61,22 @@ export const ResetTableButton = ({ tableId, tableName }: ResetTableButtonProps) - Reset and restart table "{tableName}"? - -

- This will reset and restart replication for this table only. The table will start - copying from scratch, and any existing data for this table downstream will be deleted. -

-

- Other tables in the pipeline will not be affected. Only this table will be restarted - and go through the full replication process again, starting with the initial copy - phase. -

-

- The pipeline will be restarted to apply the table reset. -

+ Reset table and restart + + This will reset replication for {tableName}{' '} + only. The table will be copied again from scratch, and any existing downstream data for + it will be deleted. Other tables in the pipeline are not affected, but the pipeline will + restart to apply this reset.
Cancel - + {isRollingBack ? 'Resetting table...' : isRestartingPipeline ? 'Restarting pipeline...' - : 'Confirm reset and restart'} + : 'Reset and restart'}
diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 6848029b954d6..d92777ca3eb77 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -21,9 +21,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { - PIPELINE_ACTIONABLE_STATES, PIPELINE_DISABLE_ALLOWED_FROM, PIPELINE_ENABLE_ALLOWED_FROM, PIPELINE_ERROR_MESSAGES, diff --git a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx index 48b5e1fe0e6f3..e64371062e56b 100644 --- a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx +++ b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx @@ -14,7 +14,6 @@ import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UpgradeToPro } from 'components/ui/UpgradeToPro' import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' import { useCloneBackupsQuery } from 'data/projects/clone-query' @@ -30,6 +29,7 @@ import { DOCS_URL, PROJECT_STATUS } from 'lib/constants' import { getDatabaseMajorVersion } from 'lib/helpers' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { PreviousRestoreItem } from './PreviousRestoreItem' export const RestoreToNewProject = () => { @@ -182,8 +182,7 @@ export const RestoreToNewProject = () => { + + + + ) +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.utils.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.utils.ts similarity index 100% rename from apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.utils.ts rename to apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.utils.ts diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx index bb8bd0c54c118..86b3609e38e2f 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx @@ -1,117 +1,6 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { useRouter } from 'next/router' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import z from 'zod' - -import { useParams } from 'common' -import { InlineLink } from 'components/ui/InlineLink' -import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation' -import { useAnalyticsBucketCreateMutation } from 'data/storage/analytics-bucket-create-mutation' -import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query' -import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { DOCS_URL } from 'lib/constants' -import { - Button, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogSection, - DialogSectionSeparator, - DialogTitle, - Form_Shadcn_, - FormControl_Shadcn_, - FormField_Shadcn_, - Input_Shadcn_, -} from 'ui' -import { Admonition } from 'ui-patterns' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { Dialog, DialogContent, DialogHeader, DialogSectionSeparator, DialogTitle } from 'ui' import { BUCKET_TYPES } from '../Storage.constants' -import { useIcebergWrapperExtension } from './AnalyticsBucketDetails/useIcebergWrapper' -import { - reservedPrefixes, - reservedSuffixes, - validBucketNameRegex, -} from './CreateAnalyticsBucketModal.utils' - -const FormSchema = z - .object({ - name: z - .string() - .trim() - .min(3, 'Bucket name should be at least 3 characters') - .max(63, 'Bucket name should be up to 63 characters') - .refine( - (value) => !value.endsWith(' '), - 'The name of the bucket cannot end with a whitespace' - ) - .refine( - (value) => value !== 'public', - '"public" is a reserved name. Please choose another name' - ), - }) - .superRefine((data, ctx) => { - if (reservedPrefixes.test(data.name)) { - const [match] = data.name.match(reservedPrefixes) ?? [] - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: `Bucket name cannot start with "${match}"`, - }) - } - - if (reservedSuffixes.test(data.name)) { - const [match] = data.name.match(reservedSuffixes) ?? [] - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: `Bucket name cannot end with "${match}"`, - }) - } - - if (/[A-Z]/.test(data.name)) { - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: 'Bucket name can only be lowercase characters', - }) - } - - if (!validBucketNameRegex.test(data.name)) { - if (!/^[a-z0-9]/.test(data.name)) { - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: 'Bucket name must start with a lowercase letter or number.', - }) - } - - if (!/[a-z0-9]$/.test(data.name)) { - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: 'Bucket name must end with a lowercase letter or number.', - }) - } - - const [match] = data.name.match(/[^a-z0-9-]/) ?? [] - return ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: !!match - ? `Bucket name cannot contain the "${match}" character` - : 'Bucket name contains an invalid special character', - }) - } - }) - -const formId = 'create-analytics-storage-bucket-form' - -export type CreateAnalyticsBucketForm = z.infer +import { CreateAnalyticsBucketForm } from './CreateAnalyticsBucketForm' interface CreateAnalyticsBucketModalProps { open: boolean @@ -122,176 +11,16 @@ export const CreateAnalyticsBucketModal = ({ open, onOpenChange, }: CreateAnalyticsBucketModalProps) => { - const router = useRouter() - const { ref } = useParams() - const { data: org } = useSelectedOrganizationQuery() - const { data: project } = useSelectedProjectQuery() - const { extension: wrappersExtension, state: wrappersExtensionState } = - useIcebergWrapperExtension() - - const { data: buckets = [] } = useAnalyticsBucketsQuery({ projectRef: ref }) - const wrappersExtenstionNeedsUpgrading = wrappersExtensionState === 'needs-upgrade' - - const { mutate: sendEvent } = useSendEventMutation() - - const { mutateAsync: createAnalyticsBucket, isPending: isCreatingAnalyticsBucket } = - useAnalyticsBucketCreateMutation({ - // [Joshen] Silencing the error here as it's being handled in onSubmit - onError: () => {}, - }) - - const { mutateAsync: createIcebergWrapper, isPending: isCreatingIcebergWrapper } = - useIcebergWrapperCreateMutation() - - const { mutateAsync: enableExtension, isPending: isEnablingExtension } = - useDatabaseExtensionEnableMutation() - const config = BUCKET_TYPES['analytics'] - const isCreating = isEnablingExtension || isCreatingIcebergWrapper || isCreatingAnalyticsBucket - - const form = useForm({ - resolver: zodResolver(FormSchema), - defaultValues: { name: '' }, - }) - - const onSubmit: SubmitHandler = async (values) => { - if (!ref) return console.error('Project ref is required') - if (!project) return console.error('Project details is required') - if (!wrappersExtension) return console.error('Unable to find wrappers extension') - - const hasExistingBucket = buckets.some((x) => x.name === values.name) - if (hasExistingBucket) return toast.error('Bucket name already exists') - - try { - await createAnalyticsBucket({ - projectRef: ref, - bucketName: values.name, - }) - - if (wrappersExtensionState === 'not-installed') { - await enableExtension({ - projectRef: project?.ref, - connectionString: project?.connectionString, - name: wrappersExtension.name, - schema: wrappersExtension.schema ?? 'extensions', - version: wrappersExtension.default_version, - }) - } - - await createIcebergWrapper({ bucketName: values.name }) - - sendEvent({ - action: 'storage_bucket_created', - properties: { bucketType: 'analytics' }, - groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, - }) - - form.reset() - toast.success(`Created bucket “${values.name}”`) - onOpenChange(false) - } catch (error: any) { - toast.error(`Failed to create bucket: ${error.message}`) - } - } - - const handleClose = () => { - form.reset() - onOpenChange(false) - } return ( - { - if (!open) handleClose() - }} - > + Create {config.singularName} bucket - - - -
- - ( - - - - - - )} - /> - - {wrappersExtenstionNeedsUpgrading ? ( - -

- Update the wrappers extension by - upgrading your project from your{' '} - - project settings - {' '} - before creating an Analytics bucket.{' '} - - Learn more - - . -

-
- ) : ( - -

- Supabase will install the{' '} - {wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''} - Iceberg Wrapper integration on your behalf.{' '} - - Learn more - - . -

-
- )} -
-
-
- - - - - +
) diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet.tsx new file mode 100644 index 0000000000000..732381fc0dfc8 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet.tsx @@ -0,0 +1,26 @@ +import { Sheet, SheetContent, SheetHeader, SheetTitle } from 'ui' +import { BUCKET_TYPES } from '../Storage.constants' +import { CreateAnalyticsBucketForm } from './CreateAnalyticsBucketForm' + +interface CreateAnalyticsBucketSheetProps { + open: boolean + onOpenChange: (value: boolean) => void +} + +export const CreateAnalyticsBucketSheet = ({ + open, + onOpenChange, +}: CreateAnalyticsBucketSheetProps) => { + const config = BUCKET_TYPES['analytics'] + + return ( + + + + Create {config.singularName} bucket + + + + + ) +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx index 19d3f328e3fc7..2c3f32eff4d60 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { AlphaNotice } from 'components/ui/AlphaNotice' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query' import { AnalyticsBucket as AnalyticsBucketIcon } from 'icons' import { BASE_PATH } from 'lib/constants' @@ -26,6 +26,7 @@ import { TimestampInfo } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { EmptyBucketState } from '../EmptyBucketState' import { CreateBucketButton } from '../NewBucketButton' import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal' @@ -41,6 +42,9 @@ export const AnalyticsBuckets = () => { parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) ) + const { data: config } = useProjectStorageConfigQuery({ projectRef: ref }) + const maxAnalyticsBuckets = config?.features.icebergCatalog.maxCatalogs ?? 2 + const { data: buckets = [], error: bucketsError, @@ -101,13 +105,12 @@ export const AnalyticsBuckets = () => { - {analyticsBuckets.length} - /2 + {analyticsBuckets.length}/{maxAnalyticsBuckets} - - Each project can only have up to 2 buckets while analytics buckets are - in alpha{' '} + + Each project can only have up to {maxAnalyticsBuckets} buckets while + analytics buckets are in alpha{' '} )} diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx index ac3d60c470226..85c93a6e4dd8d 100644 --- a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx @@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { usePaginatedBucketsQuery } from 'data/storage/buckets-query' import { IS_PLATFORM } from 'lib/constants' @@ -25,6 +24,7 @@ import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { CreateBucketModal } from '../CreateBucketModal' import { EmptyBucketState } from '../EmptyBucketState' import { CreateBucketButton } from '../NewBucketButton' diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx index 233e8b7142f55..52a77b63d192a 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -7,12 +7,12 @@ import { useContextMenu } from 'react-contexify' import { toast } from 'sonner' import { InfiniteListDefault, LoaderForIconMenuItems } from 'components/ui/InfiniteList' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Checkbox, cn } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { CONTEXT_MENU_KEYS, STORAGE_ROW_STATUS, diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx index bd53ea5f3b7ed..dcfd85b50aa2f 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx @@ -9,7 +9,6 @@ import { useFlag, useParams } from 'common' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UpgradeToPro } from 'components/ui/UpgradeToPro' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' @@ -40,6 +39,7 @@ import { import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { StorageFileSizeLimitErrorMessage } from './StorageFileSizeLimitErrorMessage' import { StorageListV2MigratingCallout, diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx index 080624cd20a87..85c98c0aa9a43 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx @@ -6,7 +6,6 @@ import { useState, type KeyboardEvent, type MouseEvent } from 'react' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { AlphaNotice } from 'components/ui/AlphaNotice' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query' import { VectorBucket as VectorBucketIcon } from 'icons' import { BASE_PATH } from 'lib/constants' @@ -14,6 +13,7 @@ import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } f import { Input } from 'ui-patterns/DataInputs/Input' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { EmptyBucketState } from '../EmptyBucketState' import { CreateBucketButton } from '../NewBucketButton' diff --git a/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx b/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx index ccbd0807b1e94..75f1257da9dd7 100644 --- a/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx +++ b/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx @@ -20,7 +20,7 @@ import { } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { useHighlightProjectRefContext } from './HighlightContext' import type { ExtendedSupportCategories } from './Support.constants' import type { SupportFormValues } from './SupportForm.schema' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index 3e50ed6ec18e6..11c008d3839af 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -66,10 +66,12 @@ export const ForeignKeySelector = ({ const hasTypeErrors = (errors.types ?? []).length > 0 const hasTypeNotices = (errors.typeNotice ?? []).length > 0 - const { data: schemas } = useSchemasQuery({ + const { data: schemas = [] } = useSchemasQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) + const sortedSchemas = [...schemas].sort((a, b) => a.name.localeCompare(b.name)) + const { data: tables } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -145,7 +147,7 @@ export const ForeignKeySelector = ({ setFk({ ...fk, [action]: value }) } - const validateSelection = (resolve: any) => { + const validateSelection = (resolve: () => void) => { const errors: SelectorErrors = {} const incompleteColumns = fk.columns.filter( (column) => column.source === '' || column.target === '' @@ -204,11 +206,13 @@ export const ForeignKeySelector = ({ if (foreignKey !== undefined) setFk(foreignKey) else setFk({ ...EMPTY_STATE, id: uuidv4() }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]) useEffect(() => { if (visible) validateType() - }, [fk]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fk, visible]) return ( validateSelection(resolve)} + applyFunction={(resolve) => validateSelection(resolve)} /> } > @@ -244,7 +248,7 @@ export const ForeignKeySelector = ({ value={fk.schema} onChange={(value: string) => updateSelectedSchema(value)} > - {schemas?.map((schema) => { + {sortedSchemas.map((schema) => { return ( void closePanel: () => void } @@ -42,6 +42,7 @@ export interface ForeignRowSelectorProps { export const ForeignRowSelector = ({ visible, foreignKey, + isSaving, onSelect, closePanel, }: ForeignRowSelectorProps) => { @@ -167,18 +168,29 @@ export const ForeignRowSelector = ({ return ( - Select a record to reference from{' '} - - {schemaName}.{tableName} - +
+

+ Select a record to reference from{' '} + + {schemaName}.{tableName} + +

+
+ {isSaving && ( +
+ +

Saving

+
+ )} +
} onCancel={closePanel} - customFooter={} >
@@ -252,15 +264,19 @@ export const ForeignRowSelector = ({ { - const value = columns?.reduce((a, b) => { - const targetColumn = selectedTable?.columns.find((x) => x.name === b.target) - const value = - targetColumn?.format === 'bytea' - ? convertByteaToHex(row[b.target]) - : row[b.target] - return { ...a, [b.source]: value } - }, {}) - onSelect(value) + if (!isSaving) { + const value = columns?.reduce((a, b) => { + const targetColumn = selectedTable?.columns.find( + (x) => x.name === b.target + ) + const value = + targetColumn?.format === 'bytea' + ? convertByteaToHex(row[b.target]) + : row[b.target] + return { ...a, [b.source]: value } + }, {}) + onSelect(value) + } }} /> ) : ( diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 80a1ba852d5b4..4485a06f332a2 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -6,7 +6,6 @@ import { toast } from 'sonner' import { useParams } from 'common' import { type GeneratedPolicy } from 'components/interfaces/Auth/Policies/Policies.utils' -import { useDatabasePolicyCreateMutation } from 'data/database-policies/database-policy-create-mutation' import { databasePoliciesKeys } from 'data/database-policies/keys' import { useDatabasePublicationCreateMutation } from 'data/database-publications/database-publications-create-mutation' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' @@ -168,7 +167,7 @@ export const SidePanelEditor = ({ toast.success('Successfully created row') }, }) - const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({ + const { mutateAsync: updateTableRow, isPending: isUpdatingRow } = useTableRowUpdateMutation({ onSuccess() { toast.success('Successfully updated row') }, @@ -177,9 +176,6 @@ export const SidePanelEditor = ({ const { mutateAsync: updatePublication } = useDatabasePublicationUpdateMutation({ onError: () => {}, }) - const { mutateAsync: createPolicy } = useDatabasePolicyCreateMutation({ - onError: () => {}, // Errors handled inline - }) const isDuplicating = snap.sidePanel?.type === 'table' && snap.sidePanel.mode === 'duplicate' @@ -785,6 +781,7 @@ export const SidePanelEditor = ({ ? snap.sidePanel.foreignKey.foreignKey : undefined } + isSaving={isUpdatingRow} closePanel={onClosePanel} onSelect={onSaveForeignRow} /> diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeysManagement.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeysManagement.tsx index 8eeb36e078314..73ac7f84752fe 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeysManagement.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeysManagement.tsx @@ -2,11 +2,11 @@ import { useState } from 'react' import { Button } from 'ui' import AlertError from 'components/ui/AlertError' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import type { ResponseError } from 'types' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ForeignKeySelector } from '../../ForeignKeySelector/ForeignKeySelector' import type { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types' import type { TableField } from '../TableEditor.types' diff --git a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx index f424dc65ba47c..74f01addd8732 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx @@ -5,7 +5,6 @@ import { useMemo, useRef } from 'react' import { useParams } from 'common' import { Footer } from 'components/grid/components/footer/Footer' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableDefinitionQuery } from 'data/database/table-definition-query' import { useViewDefinitionQuery } from 'data/database/view-definition-query' import { @@ -19,6 +18,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { formatSql } from 'lib/formatSql' import { timeout } from 'lib/helpers' import { Button } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' export interface TableDefinitionProps { entity?: Entity diff --git a/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx b/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx index 8ffd180568ad5..88d26f753c04a 100644 --- a/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/BranchDropdown.tsx @@ -12,7 +12,6 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { useParams } from 'common' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { Branch, useBranchesQuery } from 'data/branches/branches-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useAppStateSnapshot } from 'state/app-state' @@ -32,6 +31,7 @@ import { ScrollArea, cn, } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { sanitizeRoute } from './ProjectDropdown' const BranchLink = ({ diff --git a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index 812f90a6b1109..588ddfffd29ef 100644 --- a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -5,7 +5,6 @@ import { useState } from 'react' import { useParams } from 'common' import PartnerIcon from 'components/ui/PartnerIcon' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -25,6 +24,7 @@ import { ScrollArea, cn, } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export const OrganizationDropdown = () => { const router = useRouter() diff --git a/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx b/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx index b0c0af5bf81b0..a57db5640a9d2 100644 --- a/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx @@ -6,13 +6,13 @@ import { useState } from 'react' import { useParams } from 'common' import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM } from 'lib/constants' import { Button, CommandGroup_Shadcn_, CommandItem_Shadcn_, cn } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => { const queryArray = Object.entries(routerQueries) diff --git a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx index f8ccbd46b5a02..49ea48661b32a 100644 --- a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx +++ b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityMenu.tsx @@ -9,7 +9,6 @@ import { useFlag, useParams } from 'common' import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal' import { UpdateCustomReportModal } from 'components/interfaces/Reports/UpdateModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useContentDeleteMutation } from 'data/content/content-delete-mutation' import { Content, useContentQuery } from 'data/content/content-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -18,6 +17,7 @@ import { useProfile } from 'lib/profile' import { Menu, cn } from 'ui' import { InnerSideBarEmptyPanel } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ObservabilityMenuItem } from './ObservabilityMenuItem' const ObservabilityMenu = () => { diff --git a/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx b/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx index 16d774c331d4b..b498035749b36 100644 --- a/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectDetailQuery } from 'data/projects/project-detail-query' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' export const LoadingState = () => { const { ref } = useParams() diff --git a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx index 15cbe3c13e18d..8a4010e2183d0 100644 --- a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx +++ b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx @@ -9,7 +9,6 @@ import { useFlag, useParams } from 'common' import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal' import { UpdateCustomReportModal } from 'components/interfaces/Reports/UpdateModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useContentDeleteMutation } from 'data/content/content-delete-mutation' import { Content, useContentQuery } from 'data/content/content-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -17,6 +16,7 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useProfile } from 'lib/profile' import { Menu, cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ReportMenuItem } from './ReportMenuItem' const ReportsMenu = () => { diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx index 2a8e7a357d6e4..c41ee1e344219 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx @@ -10,7 +10,7 @@ import { useContentInfiniteQuery } from 'data/content/content-infinite-query' import { Snippet, SNIPPET_PAGE_LIMIT } from 'data/content/sql-folders-query' import { createTabId, useTabsStateSnapshot } from 'state/tabs' import { TreeView } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteSnippetsModal } from './DeleteSnippetsModal' import { formatFolderResponseForTreeView, getLastItemIds } from './SQLEditorNav.utils' import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem' diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerIndexAdvisor.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerIndexAdvisor.tsx index bf7bb5099d15d..b2e1fe309c673 100644 --- a/apps/studio/components/ui/BannerStack/Banners/BannerIndexAdvisor.tsx +++ b/apps/studio/components/ui/BannerStack/Banners/BannerIndexAdvisor.tsx @@ -1,11 +1,11 @@ -import { BannerCard } from '../BannerCard' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { LOCAL_STORAGE_KEYS } from 'common' import { useParams } from 'common/hooks' -import { Lightbulb } from 'lucide-react' import { EnableIndexAdvisorButton } from 'components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton' -import { useBannerStack } from '../BannerStackProvider' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useTrack } from 'lib/telemetry/track' +import { Lightbulb } from 'lucide-react' +import { BannerCard } from '../BannerCard' +import { useBannerStack } from '../BannerStackProvider' export const BannerIndexAdvisor = () => { const track = useTrack() @@ -26,7 +26,7 @@ export const BannerIndexAdvisor = () => { >
-
+
diff --git a/apps/studio/components/ui/ComputeBadgeWrapper.tsx b/apps/studio/components/ui/ComputeBadgeWrapper.tsx index 80ed25cc3de0f..a22babc88a1c6 100644 --- a/apps/studio/components/ui/ComputeBadgeWrapper.tsx +++ b/apps/studio/components/ui/ComputeBadgeWrapper.tsx @@ -10,7 +10,7 @@ import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { INSTANCE_MICRO_SPECS } from 'lib/constants' import { Button, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui' import { ComputeBadge } from 'ui-patterns/ComputeBadge' -import ShimmeringLoader from './ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' const Row = ({ label, stat }: { label: string; stat: React.ReactNode | string }) => { return ( diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx index 6ac0bf7ae4688..c2804fa1a70b4 100644 --- a/apps/studio/components/ui/FilterPopover.tsx +++ b/apps/studio/components/ui/FilterPopover.tsx @@ -2,6 +2,7 @@ import { useIntersectionObserver } from '@uidotdev/usehooks' import { noop } from 'lodash' import { X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' + import { Button, Checkbox_Shadcn_, @@ -13,7 +14,7 @@ import { ScrollArea, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface FilterPopoverProps { title?: string diff --git a/apps/studio/components/ui/OrganizationProjectSelector.tsx b/apps/studio/components/ui/OrganizationProjectSelector.tsx index be069a415d5ec..d49d54eaa35a5 100644 --- a/apps/studio/components/ui/OrganizationProjectSelector.tsx +++ b/apps/studio/components/ui/OrganizationProjectSelector.tsx @@ -21,7 +21,7 @@ import { TooltipContent, TooltipTrigger, } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface OrganizationProjectSelectorSelectorProps { slug?: string diff --git a/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx b/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx index 935c1082797e5..1a1890655e87b 100644 --- a/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx +++ b/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx @@ -113,14 +113,14 @@ export const ToggleLegacyApiKeysPanel = () => { Apps using Supabase may break Your project uses apps that integrate with Supabase. Disabling the legacy API keys is - a brand new feature and the apps you're using may not have added support for this yet. + a brand new feature and the apps you’re using may not have added support for this yet. It can cause them to stop functioning. Check before continuing. Cancel - setIsConfirmOpen(true)}> - Proceed to disable API keys + setIsConfirmOpen(true)}> + Disable API keys diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index ba211604c0301..ed3784f987ca7 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -8,7 +8,7 @@ import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartC import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { Badge, Button, ChartContainer, ChartTooltipContent, cn, CodeBlock } from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ButtonTooltip } from '../ButtonTooltip' import { CHART_COLORS } from '../Charts/Charts.constants' import { SqlWarningAdmonition } from '../SqlWarningAdmonition' diff --git a/apps/studio/components/ui/ShimmeringLoader.tsx b/apps/studio/components/ui/ShimmeringLoader.tsx deleted file mode 100644 index 948711ba0224d..0000000000000 --- a/apps/studio/components/ui/ShimmeringLoader.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useSynchronizedAnimation } from 'hooks/misc/useSynchronizedAnimation' -import { cn } from 'ui' - -// [Joshen] Deprecate this file - this is declared in ui-patterns - -export interface ShimmeringLoader { - className?: string - delayIndex?: number - animationDelay?: number -} - -export const ShimmeringLoader = ({ - className, - delayIndex = 0, - animationDelay = 150, -}: ShimmeringLoader) => { - const ref = useSynchronizedAnimation('shimmer') - - return ( -
- ) -} - -export const GenericSkeletonLoader = () => ( -
- - - -
-) -export default ShimmeringLoader diff --git a/apps/studio/components/ui/org-selector.tsx b/apps/studio/components/ui/org-selector.tsx index d563cb74a6a75..1cc858f322ad4 100644 --- a/apps/studio/components/ui/org-selector.tsx +++ b/apps/studio/components/ui/org-selector.tsx @@ -2,12 +2,12 @@ import { ChevronDown } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { parseAsString, useQueryState } from 'nuqs' import type { Organization } from 'types' import { Badge, Button, Card, CardHeader, CardTitle, Input_Shadcn_ } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ButtonTooltip } from './ButtonTooltip' export interface ProjectClaimChooseOrgProps { diff --git a/apps/studio/hooks/misc/useSynchronizedAnimation.ts b/apps/studio/hooks/misc/useSynchronizedAnimation.ts deleted file mode 100644 index 756a3ba62af25..0000000000000 --- a/apps/studio/hooks/misc/useSynchronizedAnimation.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useIsomorphicLayoutEffect } from 'common' -import { useRef } from 'react' - -// Source: https://youtu.be/3kDVachh-BM - -let stashedTime: number | null = null - -export function useSynchronizedAnimation(name: string) { - const ref = useRef(null) - - useIsomorphicLayoutEffect(() => { - const animations = document - .getAnimations() - .filter( - (animation) => animation instanceof CSSAnimation && animation.animationName === 'shimmer' - ) - - const myAnimation = animations.find( - (animation) => - animation.effect instanceof KeyframeEffect && animation.effect.target === ref.current - ) - - if (myAnimation === undefined) { - return - } - - const leadAnimation = animations[0] - - if (myAnimation === leadAnimation && stashedTime) { - myAnimation.currentTime = stashedTime - } - - if (myAnimation !== leadAnimation) { - myAnimation.currentTime = leadAnimation.currentTime - } - - return () => { - if (myAnimation === leadAnimation && myAnimation.currentTime) { - stashedTime = Number(myAnimation.currentTime) - } - } - }, []) - - return ref -} diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 3f4c83199b134..fad0d708fa8ff 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -15,6 +15,7 @@ import 'styles/stripe.scss' import 'styles/toast.scss' import 'styles/typography.scss' import 'styles/ui.scss' +import 'ui-patterns/ShimmeringLoader/index.css' import 'ui/build/css/themes/dark.css' import 'ui/build/css/themes/light.css' diff --git a/apps/studio/pages/account/me.tsx b/apps/studio/pages/account/me.tsx index 9ebc82ef3a598..d00ddf59c5b8b 100644 --- a/apps/studio/pages/account/me.tsx +++ b/apps/studio/pages/account/me.tsx @@ -11,7 +11,6 @@ import AppLayout from 'components/layouts/AppLayout/AppLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import AlertError from 'components/ui/AlertError' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useProfile } from 'lib/profile' import type { NextPageWithLayout } from 'types' @@ -24,6 +23,7 @@ import { PageHeaderSummary, PageHeaderTitle, } from 'ui-patterns/PageHeader' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const User: NextPageWithLayout = () => { return diff --git a/apps/studio/pages/authorize.tsx b/apps/studio/pages/authorize.tsx index 63ba3e39af43b..20eae2ef0ebf3 100644 --- a/apps/studio/pages/authorize.tsx +++ b/apps/studio/pages/authorize.tsx @@ -10,7 +10,6 @@ import * as z from 'zod' import { useParams } from 'common' import { AuthorizeRequesterDetails } from 'components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails' import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-authorization-approve-mutation' import { useApiAuthorizationDeclineMutation } from 'data/api-authorization/api-authorization-decline-mutation' import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' @@ -40,6 +39,7 @@ import { WarningIcon, } from 'ui' import { FormLayout } from 'ui-patterns/form/Layout/FormLayout' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' // Need to handle if no organizations in account // Need to handle if not logged in yet state diff --git a/apps/studio/pages/claim-project.tsx b/apps/studio/pages/claim-project.tsx index 400fe63f66a22..481f9b7578d9c 100644 --- a/apps/studio/pages/claim-project.tsx +++ b/apps/studio/pages/claim-project.tsx @@ -6,7 +6,6 @@ import { ProjectClaimBenefits } from 'components/interfaces/Organization/Project import { ProjectClaimChooseOrg } from 'components/interfaces/Organization/ProjectClaim/choose-org' import { ProjectClaimConfirm } from 'components/interfaces/Organization/ProjectClaim/confirm' import { ProjectClaimLayout } from 'components/interfaces/Organization/ProjectClaim/layout' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' import { useOrganizationProjectClaimQuery } from 'data/organizations/organization-project-claim-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' @@ -14,6 +13,7 @@ import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { withAuth } from 'hooks/misc/withAuth' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns/admonition' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' const ClaimProjectPageLayout = ({ children }: PropsWithChildren) => { const { appTitle } = useCustomContent(['app:title']) diff --git a/apps/studio/pages/project/[ref]/auth/audit-logs.tsx b/apps/studio/pages/project/[ref]/auth/audit-logs.tsx index 7897a65e1639d..d3d5b1725ec65 100644 --- a/apps/studio/pages/project/[ref]/auth/audit-logs.tsx +++ b/apps/studio/pages/project/[ref]/auth/audit-logs.tsx @@ -5,7 +5,6 @@ import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' @@ -19,6 +18,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const AuditLogsPage: NextPageWithLayout = () => { const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( diff --git a/apps/studio/pages/project/[ref]/auth/mfa.tsx b/apps/studio/pages/project/[ref]/auth/mfa.tsx index 3b530fb2e9969..f9237a052e10f 100644 --- a/apps/studio/pages/project/[ref]/auth/mfa.tsx +++ b/apps/studio/pages/project/[ref]/auth/mfa.tsx @@ -5,7 +5,6 @@ import { MfaAuthSettingsForm } from 'components/interfaces/Auth/MfaAuthSettingsF import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -19,6 +18,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const MfaPage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/apps/studio/pages/project/[ref]/auth/performance.tsx b/apps/studio/pages/project/[ref]/auth/performance.tsx index fdc3e3ffa4af5..745e9c45fd749 100644 --- a/apps/studio/pages/project/[ref]/auth/performance.tsx +++ b/apps/studio/pages/project/[ref]/auth/performance.tsx @@ -5,7 +5,6 @@ import { PerformanceSettingsForm } from 'components/interfaces/Auth/PerformanceS import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -19,6 +18,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const PerformancePage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index 9d455127920f2..409a55ce33d42 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -18,7 +18,6 @@ import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import SchemaSelector from 'components/ui/SchemaSelector' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useTablesQuery } from 'data/tables/tables-query' @@ -42,6 +41,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' /** * Filter tables by table name and policy name diff --git a/apps/studio/pages/project/[ref]/auth/protection.tsx b/apps/studio/pages/project/[ref]/auth/protection.tsx index 967a9b15c0ab3..7f1c666692680 100644 --- a/apps/studio/pages/project/[ref]/auth/protection.tsx +++ b/apps/studio/pages/project/[ref]/auth/protection.tsx @@ -5,7 +5,6 @@ import { ProtectionAuthSettingsForm } from 'components/interfaces/Auth/Protectio import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -19,6 +18,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const ProtectionPage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/apps/studio/pages/project/[ref]/auth/rate-limits.tsx b/apps/studio/pages/project/[ref]/auth/rate-limits.tsx index bb85a7b191150..fd626ae0ee2f0 100644 --- a/apps/studio/pages/project/[ref]/auth/rate-limits.tsx +++ b/apps/studio/pages/project/[ref]/auth/rate-limits.tsx @@ -6,7 +6,6 @@ import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -22,6 +21,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const RateLimitsPage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/apps/studio/pages/project/[ref]/auth/sessions.tsx b/apps/studio/pages/project/[ref]/auth/sessions.tsx index b1a1e5880162a..3d1751f787ada 100644 --- a/apps/studio/pages/project/[ref]/auth/sessions.tsx +++ b/apps/studio/pages/project/[ref]/auth/sessions.tsx @@ -4,7 +4,6 @@ import { SessionsAuthSettingsForm } from 'components/interfaces/Auth/SessionsAut import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' import { PageContainer } from 'ui-patterns/PageContainer' @@ -16,6 +15,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const SessionsPage: NextPageWithLayout = () => { const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( diff --git a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx index d74b75461a0e1..9f74bc06b88f0 100644 --- a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx +++ b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx @@ -5,7 +5,6 @@ import SiteUrl from 'components/interfaces/Auth/SiteUrl/SiteUrl' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' import { PageContainer } from 'ui-patterns/PageContainer' @@ -17,6 +16,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const URLConfiguration: NextPageWithLayout = () => { const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 7a255c6215827..57337c41673ff 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -10,7 +10,6 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UpgradeToPro } from 'components/ui/UpgradeToPro' import { useBackupsQuery } from 'data/database/backups-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -29,6 +28,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const DatabasePhysicalBackups: NextPageWithLayout = () => { return ( diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index 0d4a438adcccb..2ef5f010ce927 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -10,7 +10,6 @@ import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import InformationBox from 'components/ui/InformationBox' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useBackupsQuery } from 'data/database/backups-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' @@ -26,6 +25,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const DatabaseScheduledBackups: NextPageWithLayout = () => { const { ref: projectRef } = useParams() diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx index d5fb4f215f1fc..f3f6422babb15 100644 --- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx +++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx @@ -22,7 +22,6 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { PgRole, useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useColumnPrivilegesQuery } from 'data/privileges/column-privileges-query' import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query' @@ -34,6 +33,7 @@ import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role'] diff --git a/apps/studio/pages/project/[ref]/database/tables/[id].tsx b/apps/studio/pages/project/[ref]/database/tables/[id].tsx index ecb96d569db1f..3401092bacc0a 100644 --- a/apps/studio/pages/project/[ref]/database/tables/[id].tsx +++ b/apps/studio/pages/project/[ref]/database/tables/[id].tsx @@ -14,7 +14,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useTableEditorStateSnapshot } from 'state/table-editor' import { TableEditorTableStateContextProvider } from 'state/table-editor-table' import type { NextPageWithLayout } from 'types' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' const DatabaseTables: NextPageWithLayout = () => { const snap = useTableEditorStateSnapshot() diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 5ecaa0fbec678..12b93f1d7f10c 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -20,7 +20,6 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { DOCS_URL, IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' @@ -36,6 +35,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const EdgeFunctionsPage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx index e8ce35c739c4b..66b2a04684503 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx @@ -6,7 +6,6 @@ import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integra import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns' import { PageContainer } from 'ui-patterns/PageContainer' @@ -18,6 +17,7 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 2d9648034a1a3..4577175c6159a 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -7,7 +7,6 @@ import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integra import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' @@ -34,6 +33,7 @@ import { PageSection, PageSectionContent, } from 'ui-patterns' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' type NavigationItem = { label: string; href: string; active?: boolean } diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx index 7fd08777fdfcd..b310792e5b4cf 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx @@ -4,10 +4,10 @@ import { useEffect } from 'react' import { useParams } from 'common' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import type { NextPageWithLayout } from 'types' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() diff --git a/apps/studio/pages/project/[ref]/settings/jwt/index.tsx b/apps/studio/pages/project/[ref]/settings/jwt/index.tsx index 1013828e9ed5d..3b4ea47b0c5f0 100644 --- a/apps/studio/pages/project/[ref]/settings/jwt/index.tsx +++ b/apps/studio/pages/project/[ref]/settings/jwt/index.tsx @@ -5,9 +5,9 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import JWTKeysLayout from 'components/layouts/JWTKeys/JWTKeysLayout' import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' import NoPermission from 'components/ui/NoPermission' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' const JWTSigningKeysPage: NextPageWithLayout = () => { const { can: canReadAPIKeys, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( diff --git a/apps/studio/styles/storage.scss b/apps/studio/styles/storage.scss index 633b4f2f9da3f..d6368c40fae14 100644 --- a/apps/studio/styles/storage.scss +++ b/apps/studio/styles/storage.scss @@ -48,37 +48,6 @@ button[aria-haspopup='menu']:focus-visible { } } -.shimmering-loader { - animation: shimmer 2s infinite linear; - background: linear-gradient( - to right, - hsl(var(--border-default)) 4%, - hsl(var(--background-surface-200)) 25%, - hsl(var(--border-default)) 36% - ); - background-size: 1000px 100%; -} - -.dark .shimmering-loader { - animation: shimmer 2s infinite linear; - background: linear-gradient( - to right, - hsl(var(--border-default)) 4%, - hsl(var(--border-control)) 25%, - hsl(var(--border-default)) 36% - ); - background-size: 1000px 100%; -} - -@keyframes shimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - .sql-editor-container { @apply p-0; } diff --git a/apps/www/components/Forms/ApplyToSupaSquadForm.tsx b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx index d93a5a435c5eb..ac7e74a9991c4 100644 --- a/apps/www/components/Forms/ApplyToSupaSquadForm.tsx +++ b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx @@ -1,40 +1,42 @@ -import { FC, useEffect, useState, memo } from 'react' -import { AlertCircle, CheckCircle2 } from 'lucide-react' -import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' +import { AlertCircle } from 'lucide-react' +import { FC, memo, useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { Button, Form_Shadcn_, - FormField_Shadcn_, - FormLabel_Shadcn_, FormControl_Shadcn_, + FormDescription_Shadcn_, + FormField_Shadcn_, FormItem_Shadcn_, - Input_Shadcn_, + FormLabel_Shadcn_, FormMessage_Shadcn_, + Input_Shadcn_, Separator, TextArea_Shadcn_, - FormDescription_Shadcn_, } from 'ui' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorItem, + MultiSelectorList, + MultiSelectorTrigger, +} from 'ui-patterns/multi-select' import { Alert, AlertDescription } from 'ui/src/components/shadcn/ui/alert' import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, AlertDialogTitle, } from 'ui/src/components/shadcn/ui/alert-dialog' import { - MultiSelector, - MultiSelectorContent, - MultiSelectorList, - MultiSelectorTrigger, - MultiSelectorItem, -} from 'ui-patterns/multi-select' -import { CountrySelector } from '../Supasquad/CountrySelector' -import { - supaSquadApplicationSchema, SupaSquadApplication, + supaSquadApplicationSchema, } from '~/data/open-source/contributing/supasquad.utils' +import { CountrySelector } from '../Supasquad/CountrySelector' interface FormItem_Shadcn_ { type: 'text' | 'textarea' @@ -706,27 +708,17 @@ const ApplyToSupaSquadForm: FC = ({ className }) => { {/* Confirmation AlertDialog Overlay */} {}}> - - Application Submitted - - Your application has been successfully submitted. Please check your email for - confirmation. - -
-
- -
-
-

Application Submitted!

-

- Thank you for your submission. Please check your email for a confirmation link to - complete your application. -

-
- - Got it, thanks! - -
+ + + Application submitted + + Thank you for your submission! Please check your email for a confirmation link to + complete your application. + + + + Got it +
diff --git a/apps/www/components/Wrapped/Pages/ProductAnnouncements.tsx b/apps/www/components/Wrapped/Pages/ProductAnnouncements.tsx index ebb4124f88ea7..61a30b49cb3c9 100644 --- a/apps/www/components/Wrapped/Pages/ProductAnnouncements.tsx +++ b/apps/www/components/Wrapped/Pages/ProductAnnouncements.tsx @@ -18,24 +18,24 @@ const months: Month[] = [ name: 'January 2025', announcements: [ { - title: 'Third-party Auth providers are now GA', - url: 'https://supabase.link/docs-third-party-auth-ga-jan2025', + title: 'Third-party Auth with Firebase is now GA', + url: 'https://supabase.com/docs/guides/auth/third-party/firebase-auth', }, { - title: 'Easier errors and logs', - url: 'https://supabase.link/link-easier-errors-logs-jan2025', + title: 'Easier to see errors in log charts', + url: 'https://github.com/supabase/supabase/pull/32742', }, { - title: 'Enhanced JSON types', - url: 'https://supabase.link/github-enhanced-json-types-jan2025', + title: 'Enhanced type inference for JSON fields', + url: 'https://github.com/orgs/supabase/discussions/32925', }, { - title: 'New Supabase integrations', - url: 'https://supabase.link/link-new-supabase-integrations-jan2025', + title: 'Type validation for query filter values', + url: 'https://supabase.com/docs/guides/api/rest/generating-types', }, { - title: 'Performance improvements for Storage CDN', - url: 'https://supabase.link/link-performance-improvements-storage-cdn-jan2025', + title: 'AI Prompt for writing Edge Functions', + url: 'https://supabase.com/docs/guides/getting-started/ai-prompts/edge-functions', }, ], }, @@ -43,20 +43,44 @@ const months: Month[] = [ name: 'February 2025', announcements: [ { - title: 'Deploy Edge Functions via GitHub', - url: 'https://supabase.link/link-deploy-edge-functions-feb2025-riur', + title: 'Deploy Edge Functions from the Supabase dashboard', + url: 'https://x.com/kiwicopple/status/1889031271801905543', }, { - title: 'Connect AI agents to Supabase', - url: 'https://supabase.link/link-connect-ai-supabase-feb2025-nwod', + title: 'Deploy Edge Functions from the CLI', + url: 'https://x.com/kiwicopple/status/1890284547897716762', }, { - title: 'Enhanced JSON query operators', - url: 'https://supabase.link/link-enhanced-json-query-operators-feb2025-d6ho', + title: 'Deploy Edge Functions using the API', + url: 'https://x.com/kiwicopple/status/1892394059559231728', }, { - title: 'New community integrations', - url: 'https://supabase.link/link-new-community-integrations-feb2025-hrhx', + title: 'Connect AI tools and LLMs to Supabase', + url: 'https://supabase.com/docs/guides/getting-started/mcp', + }, + { + title: 'Third-party Auth is now a lot cheaper', + url: 'https://github.com/orgs/supabase/discussions/33959', + }, + { + title: 'New billing documentation', + url: 'https://supabase.com/docs/guides/platform/billing-on-supabase', + }, + { + title: 'Using Postgres as a Graph Database', + url: 'https://supabase.com/blog/pgrouting-postgres-graph-database', + }, + { + title: 'HubSpot Foreign Data Wrapper', + url: 'https://fdw.dev/catalog/hubspot', + }, + { + title: 'Notion Foreign Data Wrapper', + url: 'https://fdw.dev/catalog/notion', + }, + { + title: 'SQL Editor in Dashboard', + url: 'https://github.com/orgs/supabase/discussions/33835', }, ], }, @@ -66,51 +90,39 @@ const months: Month[] = [ announcements: [ { title: 'Supabase MCP Server', - url: 'https://supabase.link/link-supabase-mcp-server-mar2025-67zo', + url: 'https://supabase.com/blog/mcp-server', }, { title: 'Supabase UI Library', - url: 'https://supabase.link/link-supabase-ui-library-mar2025-sy2o', - }, - { - title: 'Supabase Templates', - url: 'https://supabase.link/link-supabase-templates-mar2025-fqbo', - }, - { - title: 'Queue: Postgres-native Job Queue', - url: 'https://supabase.link/link-postgres-queue-mar2025-zrz3', - }, - { - title: 'Supabase Logs: Open-Source Logging Infrastructure', - url: 'https://supabase.link/link-supabase-logs-mar2025-oeuf', + url: 'https://supabase.com/blog/supabase-ui-library', }, { - title: 'Anonymous Sign-ins', - url: 'https://supabase.link/link-anon-sign-ins-mar2025-g4ms', + title: 'Supabase Studio Improvements', + url: 'https://supabase.com/blog/tabs-dashboard-updates', }, { - title: 'Storage: Resumable Uploads', - url: 'https://supabase.link/link-storage-resumable-uploads-mar2025-f3ds', + title: 'Edge Functions Deploy from the Supabase Dashboard', + url: 'https://supabase.com/blog/supabase-edge-functions-deploy-dashboard-deno-2-1', }, { - title: 'Custom Access Tokens', - url: 'https://supabase.link/link-custom-access-tokens-mar2025-9u9z', + title: 'Realtime Broadcast from Database', + url: 'https://supabase.com/blog/realtime-broadcast-from-database', }, { - title: 'Realtime: Broadcast from Database', - url: 'https://supabase.link/link-realtime-broadcast-database-mar2025-6lhq', + title: 'Declarative Schemas', + url: 'https://supabase.com/blog/declarative-schemas', }, { - title: 'Database Webhooks', - url: 'https://supabase.link/link-database-webhooks-mar2025-bvj9', + title: 'Postgres Language Server', + url: 'https://supabase.com/blog/postgres-language-server', }, { - title: 'Functions: Background Tasks', - url: 'https://supabase.link/link-functions-background-tasks-mar2025-4hf3', + title: 'Clerk Support in Third-Party Auth', + url: 'https://supabase.com/blog/clerk-tpa-pricing', }, { - title: 'Postgres Language Server', - url: 'https://supabase.link/link-postgres-language-server-mar2025-gvt7', + title: 'Dedicated Poolers', + url: 'https://supabase.com/blog/dedicated-poolers', }, ], }, @@ -118,20 +130,28 @@ const months: Month[] = [ name: 'April 2025', announcements: [ { - title: 'Project-scoped roles', - url: 'https://supabase.link/github-project-scoped-roles-apr2025-k2mz', + title: 'Project scoped roles', + url: 'https://github.com/orgs/supabase/discussions/35172', + }, + { + title: 'MCP Server now works with VS Code', + url: 'https://x.com/kiwicopple/status/1911945478629179504', }, { - title: 'MCP Server + VSCode setup', - url: 'https://supabase.link/twitter-mcp-server-vscode-setup-apr2025-0580', + title: 'MCP Server can now create and deploy Edge Functions', + url: 'https://x.com/dshukertjr/status/1917927485024449006', }, { - title: 'AI Agent integrations', - url: 'https://supabase.link/link-ai-agent-integrations-apr2025-8jkl', + title: 'Supabase UI Library now includes Infinite Query block', + url: 'https://supabase.com/ui/docs/infinite-query-hook', }, { - title: 'Database branching improvements', - url: 'https://supabase.link/link-database-branching-apr2025-3mnp', + title: 'Supabase UI Library now includes Social Auth', + url: 'https://supabase.com/ui/docs/nextjs/social-auth', + }, + { + title: 'New SOC2 Report', + url: 'https://supabase.com/features/soc-2-compliance', }, ], }, @@ -139,16 +159,16 @@ const months: Month[] = [ name: 'May 2025', announcements: [ { - title: 'Dashboard visual update', - url: 'https://supabase.link/twitter-supabase-dashboard-update-may2025-6btg', + title: 'New Supabase Dashboard homepage', + url: 'https://x.com/kiwicopple/status/1922625094506967457', }, { - title: 'Figma + Make + Supabase workflow', - url: 'https://supabase.link/twitter-figma-make-supabase-may2025-7kjy', + title: 'Figma Make supports Supabase', + url: 'https://x.com/figma/status/1920169817807728834', }, { - title: 'Edge Functions monitoring', - url: 'https://supabase.link/link-edge-functions-monitoring-may2025-9qrs', + title: 'Index Advisor', + url: 'https://x.com/kiwicopple/status/1924414039700001142', }, ], }, @@ -156,26 +176,61 @@ const months: Month[] = [ name: 'August 2025', isLaunchWeek: true, announcements: [ - { title: 'JWT Signing Keys', url: 'https://supabase.com/blog/jwt-signing-keys' }, - { title: 'Automatic Embeddings', url: 'https://supabase.com/blog/automatic-embeddings' }, - { title: 'Storage v4', url: 'https://supabase.com/blog/storage-v4' }, - { title: 'Realtime 3.0', url: 'https://supabase.com/blog/realtime-broadcast-authorization' }, { - title: 'pg_replicate: Build Postgres Replicas', - url: 'https://supabase.com/blog/pg-replicate', + title: 'New API Keys + JWT Signing Keys', + url: 'https://supabase.com/blog/jwt-signing-keys', + }, + { + title: 'Analytics Buckets with Apache Iceberg Support', + url: 'https://supabase.com/blog/analytics-buckets', + }, + { + title: 'New Observability Features in Supabase', + url: 'https://supabase.com/blog/new-observability-features-in-supabase', + }, + { + title: 'Build with Figma Make and Supabase', + url: 'https://supabase.com/blog/figma-make-support-for-supabase', + }, + { + title: '10X Larger Supabase Storage Uploads, 3X Cheaper Egress', + url: 'https://supabase.com/blog/storage-500gb-uploads-cheaper-egress-pricing', + }, + { + title: 'Edge Functions: Persistent Storage and 97% Faster Boot Times', + url: 'https://supabase.com/blog/persistent-storage-for-faster-edge-functions', }, - { title: 'Supavisor 2.0', url: 'https://supabase.com/blog/supavisor-v2' }, { - title: 'Edge Functions: Deploy Previews', - url: 'https://supabase.com/blog/edge-functions-deploy-previews', + title: 'Improved Security Controls and a New Home for Security', + url: 'https://supabase.com/blog/improved-security-controls', }, - { title: 'Logs: Sources and Sinks', url: 'https://supabase.com/blog/log-drains' }, - { title: 'Database Migration UI', url: 'https://supabase.com/blog/database-migrations-ui' }, - { title: 'AI Assistant v2', url: 'https://supabase.com/blog/ai-assistant-v2' }, - { title: 'Supabase Studio 2.0', url: 'https://supabase.com/blog/supabase-studio-2' }, { - title: 'Auth Helpers Everywhere', - url: 'https://supabase.com/blog/auth-helpers-everywhere', + title: 'Branching 2.0: GitHub Optional', + url: 'https://supabase.com/blog/branching-2-0', + }, + { + title: 'Supabase UI: Platform Kit', + url: 'https://supabase.com/blog/supabase-ui-platform-kit', + }, + { + title: 'Stripe-To-Postgres Sync Engine as an NPM Package', + url: 'https://supabase.com/blog/stripe-engine-as-sync-library', + }, + { + title: 'Algolia Connector for Supabase', + url: 'https://supabase.com/blog/algolia-connector-for-supabase', + }, + { + title: 'MCP Server Can Query Docs', + url: 'https://supabase.com/docs/guides/getting-started/mcp', + }, + { + title: 'Iceberg Foreign Data Wrapper', + url: 'https://supabase.com/docs/guides/database/extensions/wrappers/iceberg', + }, + { + title: 'DuckDB Foreign Data Wrapper', + url: 'https://supabase.com/docs/guides/database/extensions/wrappers/duckdb', }, ], }, @@ -183,27 +238,37 @@ const months: Month[] = [ name: 'September 2025', announcements: [ { - title: 'Broadcast Replay', - url: 'https://supabase.com/docs/guides/realtime/broadcast#broadcast-replay', + title: '3X Cheaper Egress for Cache Hits', + url: 'https://github.com/orgs/supabase/discussions/38119', + }, + { + title: 'Expiring personal access tokens', + url: 'https://supabase.com/dashboard/account/tokens', // Direct dashboard link (intentional) + }, + { + title: 'Self-service SSO for Teams + Enterprise', + url: 'https://supabase.com/docs/guides/platform/sso', }, { - title: 'Performance Advisor improvements', - url: 'https://github.com/orgs/supabase/discussions/33287', + title: 'Deno 2.1 in All Regions', + url: 'https://github.com/orgs/supabase/discussions/37941', }, - { title: 'MCP Server updates', url: 'https://supabase.com/docs/guides/ai/mcp' }, ], }, { name: 'October 2025', announcements: [ { - title: 'Supabase Series E announcement', - url: 'https://supabase.com/blog/supabase-series-e', + title: 'Supabase Remote MCP Server', + url: 'https://supabase.com/blog/remote-mcp-server', + }, + { + title: 'Login with Solana and Ethereum', + url: 'https://supabase.com/blog/login-with-solana-ethereum', }, - { title: 'Remote MCP Server', url: 'https://supabase.com/blog/remote-mcp-server' }, { - title: 'Enhanced database observability', - url: 'https://supabase.com/docs/guides/platform/metrics', + title: 'Supabase Javascript Library MonoRepo', + url: 'https://github.com/orgs/supabase/discussions/39197', }, ], }, @@ -211,16 +276,12 @@ const months: Month[] = [ name: 'November 2025', announcements: [ { - title: 'Realtime Broadcast Replay', + title: 'Realtime Replay, available in alpha', url: 'https://supabase.com/blog/realtime-broadcast-replay', }, { - title: 'Auth email template customization', - url: 'https://supabase.com/docs/guides/auth/auth-email-templates', - }, - { - title: 'Edge Functions cold start improvements', - url: 'https://supabase.com/docs/guides/functions', + title: 'Log Drains in Self-Hosted Supabase', + url: 'https://supabase.com/docs/guides/telemetry/log-drains', }, ], }, @@ -228,28 +289,48 @@ const months: Month[] = [ name: 'December 2025', isLaunchWeek: true, announcements: [ - { title: 'Supabase ETL', url: 'https://supabase.com/blog/introducing-supabase-etl' }, + { + title: 'Supabase ETL', + url: 'https://supabase.com/blog/introducing-supabase-etl', + }, { title: 'Analytics Buckets', url: 'https://supabase.com/blog/introducing-analytics-buckets', }, - { title: 'Vector Buckets', url: 'https://supabase.com/blog/vector-buckets' }, - { title: 'iceberg-js', url: 'https://supabase.com/blog/introducing-iceberg-js' }, + { + title: 'Vector Buckets', + url: 'https://supabase.com/blog/vector-buckets', + }, + { + title: 'iceberg-js', + url: 'https://supabase.com/blog/introducing-iceberg-js', + }, { title: 'Supabase for Platforms', url: 'https://supabase.com/blog/introducing-supabase-for-platforms', }, { - title: 'Sign in with Your App (OAuth2 Provider)', - url: 'https://supabase.com/blog/oauth2-provider', + title: 'New Auth Templates', + url: 'https://supabase.com/docs/guides/auth/auth-email-templates', + }, + { + title: 'Sign in with Your App', + url: 'https://www.supabase.com/blog/oauth2-provider', + }, + { + title: 'Supabase power for Amazon Kiro', + url: 'https://supabase.com/blog/supabase-power-for-kiro', + }, + { + title: 'Supabase in the AWS Marketplace', + url: 'https://www.linkedin.com/posts/paulcopplestone_you-can-now-purchase-supabase-through-the-activity-7392589414666792960-PAvn', }, - { title: 'Supabase for Kiro IDE', url: 'https://supabase.com/blog/supabase-power-for-kiro' }, { - title: 'Async Streaming for Postgres Foreign Data Wrappers', - url: 'https://supabase.com/blog/async-postgres-fdws', + title: 'Async Streaming to Postgres FDWs', + url: 'https://www.supabase.com/blog/adding-async-streaming-to-pg-fdw', }, { - title: 'Own Your Observability: Supabase Metrics API', + title: 'Supabase Metrics API', url: 'https://supabase.com/blog/metrics-api-observability', }, ], diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index d6455eb51527d..1446e3a148022 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1300,8 +1300,8 @@ module.exports = [ }, { permanent: true, - source: '/docs/guides/auth/auth-helpers/auth-ui-overview', - destination: '/docs/guides/auth/auth-helpers/auth-ui', + source: '/docs/guides/auth/auth-helpers/:path*', + destination: '/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers', }, { permanent: false, @@ -2157,11 +2157,7 @@ module.exports = [ source: '/customers/mendableai', destination: '/customers/firecrawl', }, - { - permanent: true, - source: '/docs/guides/auth/auth-helpers/nextjs-server-components', - destination: '/docs/guides/auth/auth-helpers/nextjs', - }, + { permanent: true, source: '/docs/guides/getting-started/openai/vector-search', diff --git a/docker/reset.sh b/docker/reset.sh index d5f3a41dae066..af0302a8227ee 100755 --- a/docker/reset.sh +++ b/docker/reset.sh @@ -1,44 +1,76 @@ -#!/bin/bash - -echo "WARNING: This will remove all containers and container data, and will reset the .env file. This action cannot be undone!" -read -p "Are you sure you want to proceed? (y/N) " -n 1 -r -echo # Move to a new line -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - echo "Operation cancelled." - exit 1 +#!/bin/sh + +set -e + +auto_confirm=0 + +confirm () { + if [ "$auto_confirm" = "1" ]; then + return 0 + fi + + printf "Are you sure you want to proceed? (y/N) " + read -r REPLY + case "$REPLY" in + [Yy]) + ;; + *) + echo "Script canceled." + exit 1 + ;; + esac +} + +if [ "$1" = "-y" ]; then + auto_confirm=1 fi -echo "Stopping and removing all containers..." -docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans - -echo "Cleaning up bind-mounted directories..." -BIND_MOUNTS=( - "./volumes/db/data" -) - -for DIR in "${BIND_MOUNTS[@]}"; do - if [ -d "$DIR" ]; then - echo "Deleting $DIR..." - rm -rf "$DIR" - else - echo "Directory $DIR does not exist. Skipping bind mount deletion step..." - fi -done +echo "" +echo "*** WARNING: This will remove all containers and container data, and optionally reset .env ***" +echo "" + +confirm + +echo "===> Stopping and removing all containers..." -echo "Resetting .env file..." if [ -f ".env" ]; then - echo "Removing existing .env file..." - rm -f .env + docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans +elif [ -f ".env.example" ]; then + echo "No .env found, using .env.example for docker compose down..." + docker compose --env-file .env.example -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans +else + echo "Skipping 'docker compose down' because there's no env-file." +fi + +echo "===> Cleaning up bind-mounted directories..." +BIND_MOUNTS="./volumes/db/data ./volumes/storage" + +for dir in $BIND_MOUNTS; do + if [ -d "$dir" ]; then + echo "Removing $dir..." + confirm + rm -rf "$dir" + else + echo "$dir not found." + fi +done + +echo "===> Resetting .env file (will save backup to .env.old)..." +confirm +if [ -f ".env" ] || [ -L ".env" ]; then + echo "Renaming existing .env file to .env.old" + mv .env .env.old else - echo "No .env file found. Skipping .env removal step..." + echo "No .env file found." fi if [ -f ".env.example" ]; then - echo "Copying .env.example to .env..." - cp .env.example .env + echo "===> Copying .env.example to .env" + cp .env.example .env else - echo ".env.example file not found. Skipping .env reset step..." + echo "No .env.example found, can't restore .env to default values." fi -echo "Cleanup complete!" \ No newline at end of file +echo "Cleanup complete!" +echo "Re-run 'docker compose pull' to update images." +echo "" diff --git a/docker/utils/db-passwd.sh b/docker/utils/db-passwd.sh new file mode 100644 index 0000000000000..40c9122f37338 --- /dev/null +++ b/docker/utils/db-passwd.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# +# Portions of this code are derived from Inder Singh's update-db-pass.sh +# Copyright 2025 Inder Singh. Licensed under Apache License 2.0. +# Original source: +# https://github.com/singh-inder/supabase-automated-self-host/blob/main/docker/update-db-pass.sh +# +# GitHub discussion here: +# https://github.com/supabase/supabase/issues/22605#issuecomment-3323382144 +# +# Changed: +# - POSIX shell compatibility +# - No hardcoded values for database service and admin user +# - Use .env for the admin user and database service port +# - Does _not_ set password for supabase_read_only_user (this role is not +# supposed to have a password) +# - Print all values and confirm before updating +# - Stop on any errors +# +# Heads up: +# - Updating _analytics.source_backends is not needed after PR logflare#2069 +# - Newer Logflare versions use a different table and update connection string +# + +set -e + +if ! docker compose version > /dev/null 2>&1; then + echo "Docker Compose not found." + exit 1 +fi + +if [ ! -f .env ]; then + echo "Missing .env file. Exiting." + exit 1 +fi + +# Generate random hex-only password to avoid issues with SQL/shell +new_passwd="$(openssl rand -hex 16)" +# If replacing with a custom password, avoid using @/?#:& +# https://supabase.com/docs/guides/database/postgres/roles#passwords +# new_passwd="d0notUseSpecialSymbolsForPq123-" + +# Check Postgres service +db_image_prefix="supabase.postgres:" + +compose_output=$(docker compose ps \ + --format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null | \ + grep -m1 "^$db_image_prefix" || true) + +if [ -z "$compose_output" ]; then + echo "Postgres container not found. Exiting." + exit 1 +fi + +db_image=$(echo "$compose_output" | cut -f1) +db_srv_name=$(echo "$compose_output" | cut -f2) +db_srv_status=$(echo "$compose_output" | cut -f3) + +case "$db_srv_status" in + Up*) + ;; + *) + echo "Postgres container status: $db_srv_status" + echo "Exiting." + exit 1 + ;; +esac + +db_srv_port=$(grep "^POSTGRES_PORT=" .env | cut -d '=' -f 2) +port_source=" (.env):" +if [ -z "$db_srv_port" ]; then + db_srv_port="5432" + port_source=" (default):" +fi + +db_admin_user="supabase_admin" + +echo "" +echo "*** Check configuration below before updating database passwords! ***" +echo "" +echo "Service name: $db_srv_name" +echo "Service status: $db_srv_status" +echo "Service port${port_source} $db_srv_port" +echo "Image: $db_image" +echo "" +echo "Admin user: $db_admin_user" + +if ! test -t 0; then + echo "" + echo "Running non-interactively. Not updating passwords." + exit 0 +fi + +echo "New database password: $new_passwd" +echo "" + +printf "Update database passwords? (y/N) " +read -r REPLY +case "$REPLY" in + [Yy]) + ;; + *) + echo "Canceled. Not updating passwords." + exit 0 + ;; +esac + +echo "Updating passwords..." +echo "Connecting to the database service container..." + +docker compose exec -T "$db_srv_name" psql -U "$db_admin_user" -d "_supabase" -v ON_ERROR_STOP=1 </dev/null 2>&1; then + echo "Error: openssl is required but not found." + exit 1 +fi + +jwt_secret="$(gen_base64 30)" + +# Used in get_token() +header='{"alg":"HS256","typ":"JWT"}' +iat=$(date +%s) +exp=$((iat + 5 * 3600 * 24 * 365)) # 5 years + +# Normalizes JSON formatting so that the token matches https://www.jwt.io/ results +anon_payload="{\"role\":\"anon\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}" +service_role_payload="{\"role\":\"service_role\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}" + +#echo "anon_payload=$anon_payload" +#echo "service_role_payload=$service_role_payload" + +anon_key=$(gen_token "$anon_payload") +service_role_key=$(gen_token "$service_role_payload") + +secret_key_base=$(gen_base64 48) +vault_enc_key=$(gen_hex 16) +pg_meta_crypto_key=$(gen_base64 24) + +logflare_public_access_token=$(gen_base64 24) +logflare_private_access_token=$(gen_base64 24) + +s3_protocol_access_key_id=$(gen_hex 16) +s3_protocol_access_key_secret=$(gen_hex 32) + +echo "" +echo "JWT_SECRET=${jwt_secret}" +echo "" +#echo "Issued at: $iat" +#echo "Expire: $exp" +echo "ANON_KEY=${anon_key}" +echo "SERVICE_ROLE_KEY=${service_role_key}" +echo "" +echo "SECRET_KEY_BASE=${secret_key_base}" +echo "VAULT_ENC_KEY=${vault_enc_key}" +echo "PG_META_CRYPTO_KEY=${pg_meta_crypto_key}" +echo "LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}" +echo "LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}" +echo "S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}" +echo "S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}" +echo "" + +postgres_password=$(gen_hex 16) +dashboard_password=$(gen_hex 16) + +echo "POSTGRES_PASSWORD=${postgres_password}" +echo "DASHBOARD_PASSWORD=${dashboard_password}" +echo "" + +if ! test -t 0; then + echo "Running non-interactively. Skipping .env update." + exit 0 +fi + +printf "Update .env file? (y/N) " +read -r REPLY +case "$REPLY" in + [Yy]) + ;; + *) + echo "Not updating .env" + exit 0 + ;; +esac + +echo "Updating .env..." + +sed \ + -i.old \ + -e "s|^JWT_SECRET=.*$|JWT_SECRET=${jwt_secret}|" \ + -e "s|^ANON_KEY=.*$|ANON_KEY=${anon_key}|" \ + -e "s|^SERVICE_ROLE_KEY=.*$|SERVICE_ROLE_KEY=${service_role_key}|" \ + -e "s|^SECRET_KEY_BASE=.*$|SECRET_KEY_BASE=${secret_key_base}|" \ + -e "s|^VAULT_ENC_KEY=.*$|VAULT_ENC_KEY=${vault_enc_key}|" \ + -e "s|^PG_META_CRYPTO_KEY=.*$|PG_META_CRYPTO_KEY=${pg_meta_crypto_key}|" \ + -e "s|^LOGFLARE_PUBLIC_ACCESS_TOKEN=.*$|LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}|" \ + -e "s|^LOGFLARE_PRIVATE_ACCESS_TOKEN=.*$|LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}|" \ + -e "s|^S3_PROTOCOL_ACCESS_KEY_ID=.*$|S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}|" \ + -e "s|^S3_PROTOCOL_ACCESS_KEY_SECRET=.*$|S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}|" \ + -e "s|^POSTGRES_PASSWORD=.*$|POSTGRES_PASSWORD=${postgres_password}|" \ + -e "s|^DASHBOARD_PASSWORD=.*$|DASHBOARD_PASSWORD=${dashboard_password}|" \ + .env diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index b1488e4b19037..2f6368f3ca179 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -2094,6 +2094,10 @@ export interface components { external_workos_enabled: boolean | null external_workos_secret: string | null external_workos_url: string | null + external_x_client_id: string | null + external_x_email_optional: boolean | null + external_x_enabled: boolean | null + external_x_secret: string | null external_zoom_client_id: string | null external_zoom_email_optional: boolean | null external_zoom_enabled: boolean | null @@ -3676,6 +3680,10 @@ export interface components { external_workos_enabled?: boolean | null external_workos_secret?: string | null external_workos_url?: string | null + external_x_client_id?: string | null + external_x_email_optional?: boolean | null + external_x_enabled?: boolean | null + external_x_secret?: string | null external_zoom_client_id?: string | null external_zoom_email_optional?: boolean | null external_zoom_enabled?: boolean | null diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 5e1cc1f73fa72..55b521a2b8ad3 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -1226,26 +1226,6 @@ export interface paths { patch?: never trace?: never } - '/platform/organizations/{slug}/entitlements/entitlements': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Get entitlements for an organization - * @description Returns the entitlements available to the organization based on their plan and any overrides. - */ - get: operations['OrganizationEntitlementsController_getEntitlements'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/platform/organizations/{slug}/members': { parameters: { query?: never @@ -4345,40 +4325,6 @@ export interface paths { patch?: never trace?: never } - '/platform/telemetry/page': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** Send server page event */ - post: operations['TelemetryPageController_sendServerPageV2'] - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/platform/telemetry/page-leave': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** Send analytics page leave event */ - post: operations['TelemetryPageLeaveController_trackPageLeave'] - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/platform/telemetry/reset': { parameters: { query?: never @@ -5423,15 +5369,17 @@ export interface components { name: string organization_slugs?: string[] permissions: ( + | 'available_regions_read' | 'organizations_read' - | 'organizations_write' + | 'organizations_create' | 'projects_read' - | 'available_regions_read' | 'snippets_read' | 'organization_admin_read' | 'organization_admin_write' | 'members_read' | 'members_write' + | 'organization_projects_read' + | 'organization_projects_create' | 'project_admin_read' | 'project_admin_write' | 'advisors_read' @@ -6966,6 +6914,7 @@ export interface components { | 'branching_limit' | 'branching_persistent' | 'auth.mfa_phone' + | 'auth.mfa_web_authn' | 'auth.hooks' | 'auth.platform.sso' | 'backup.retention_days' @@ -7871,14 +7820,14 @@ export interface components { | 'billing:payment_methods' | 'realtime:all' )[] - first_name: string - free_project_limit: number + first_name: string | null + free_project_limit: number | null gotrue_id: string id: number is_alpha_user: boolean is_sso_user: boolean - last_name: string - mobile: string + last_name: string | null + mobile: string | null primary_email: string username: string } @@ -9218,40 +9167,11 @@ export interface components { reset_project?: boolean } TelemetryIdentifyBodyV2: { - /** PostHog JS SDK's distinct_id - used for aliasing anonymous → authenticated users */ anonymous_id?: string organization_slug?: string project_ref?: string user_id: string } - TelemetryPageBodyV2: { - feature_flags?: { - [key: string]: unknown - } - groups?: { - organization?: string - project?: string - } - page_title: string - page_url: string - pathname: string - ph: { - language: string - referrer: string - search: string - user_agent: string - viewport_height: number - viewport_width: number - } - } - TelemetryPageLeaveBody: { - feature_flags?: { - [key: string]: unknown - } - page_title: string - page_url: string - pathname: string - } TemporaryApiKeyResponse: { api_key: string } @@ -9658,6 +9578,10 @@ export interface components { EXTERNAL_WORKOS_ENABLED?: boolean | null EXTERNAL_WORKOS_SECRET?: string | null EXTERNAL_WORKOS_URL?: string | null + EXTERNAL_X_CLIENT_ID?: string | null + EXTERNAL_X_EMAIL_OPTIONAL?: boolean | null + EXTERNAL_X_ENABLED?: boolean | null + EXTERNAL_X_SECRET?: string | null EXTERNAL_ZOOM_CLIENT_ID?: string | null EXTERNAL_ZOOM_EMAIL_OPTIONAL?: boolean | null EXTERNAL_ZOOM_ENABLED?: boolean | null @@ -14068,49 +13992,6 @@ export interface operations { } } } - OrganizationEntitlementsController_getEntitlements: { - parameters: { - query?: never - header?: never - path: { - /** @description Organization slug */ - slug: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['ListEntitlementsResponse'] - } - } - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown - } - content?: never - } - /** @description Forbidden action */ - 403: { - headers: { - [name: string]: unknown - } - content?: never - } - /** @description Rate limit exceeded */ - 429: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } MembersController_getMembers: { parameters: { query?: never @@ -25813,63 +25694,6 @@ export interface operations { } } } - TelemetryPageController_sendServerPageV2: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TelemetryPageBodyV2'] - } - } - responses: { - 201: { - headers: { - [name: string]: unknown - } - content?: never - } - /** @description Failed to send server page event */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - TelemetryPageLeaveController_trackPageLeave: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TelemetryPageLeaveBody'] - } - } - responses: { - /** @description Page leave event sent */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - /** @description Failed to send analytics page leave event */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } TelemetryResetController_reset: { parameters: { query?: never diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 42f3385322ef4..7ca9f7ed8a354 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -43,15 +43,14 @@ export const TelemetryTagManager = () => { //--- // PAGE TELEMETRY //--- -export function handlePageTelemetry( +function handlePageTelemetry( API_URL: string, pathname?: string, featureFlags?: { [key: string]: unknown }, slug?: string, - ref?: string, - telemetryDataOverride?: components['schemas']['TelemetryPageBodyV2'] + ref?: string ) { // Send to PostHog client-side (only in browser) if (typeof window !== 'undefined') { @@ -197,15 +196,7 @@ export const PageTelemetry = ({ if (telemetryCookie) { try { const encodedData = telemetryCookie.split('=')[1] - const telemetryData = JSON.parse(decodeURIComponent(encodedData)) - handlePageTelemetry( - API_URL, - pathnameRef.current, - featureFlagsRef.current, - slug, - ref, - telemetryData - ) + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref) // remove the telemetry cookie document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` } catch (error) { diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 2b253af7961ad..1bec804ac3423 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -1,6 +1,8 @@ /** - * The components are listed here so that VsCode can find out about them and list them as import suggestions. Don't - * import directly from here. + * The components are listed here so that VsCode can find out about them and list them as import suggestions. + * + * DO NOT import directly from here. + * DO NOT add new components to this file. */ export * from './src/admonition' export * from './src/AssistantChat/AssistantChatForm' @@ -17,8 +19,8 @@ export * from './src/PageContainer' export * from './src/PageHeader' export * from './src/PageSection' export * from './src/PopupFrame' +export * from './src/PromoToast' +export * from './src/Row' export * from './src/ShimmeringLoader' export * from './src/TimestampInfo' export * from './src/Toc' -export * from './src/PromoToast' -export * from './src/Row' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index e55f201de6d75..06fb110b8b9ba 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -50,6 +50,18 @@ "import": "./src/Banners/index.ts", "types": "./src/Banners/index.ts" }, + "./Chart/charts/chart-bar": { + "import": "./src/Chart/charts/chart-bar.tsx", + "types": "./src/Chart/charts/chart-bar.tsx" + }, + "./Chart/charts/chart-line": { + "import": "./src/Chart/charts/chart-line.tsx", + "types": "./src/Chart/charts/chart-line.tsx" + }, + "./Chart": { + "import": "./src/Chart/index.tsx", + "types": "./src/Chart/index.tsx" + }, "./CommandMenu/api/Badges": { "import": "./src/CommandMenu/api/Badges.tsx", "types": "./src/CommandMenu/api/Badges.tsx" @@ -286,6 +298,10 @@ "import": "./src/Dialogs/TextConfirmModal.tsx", "types": "./src/Dialogs/TextConfirmModal.tsx" }, + "./EmptyStatePresentational": { + "import": "./src/EmptyStatePresentational/index.tsx", + "types": "./src/EmptyStatePresentational/index.tsx" + }, "./ExpandableVideo": { "import": "./src/ExpandableVideo/index.tsx", "types": "./src/ExpandableVideo/index.tsx" @@ -402,6 +418,10 @@ "import": "./src/McpUrlBuilder/types.ts", "types": "./src/McpUrlBuilder/types.ts" }, + "./McpUrlBuilder/utils/createMcpCopyHandler": { + "import": "./src/McpUrlBuilder/utils/createMcpCopyHandler.ts", + "types": "./src/McpUrlBuilder/utils/createMcpCopyHandler.ts" + }, "./McpUrlBuilder/utils/getMcpButtonData": { "import": "./src/McpUrlBuilder/utils/getMcpButtonData.ts", "types": "./src/McpUrlBuilder/utils/getMcpButtonData.ts" @@ -410,6 +430,10 @@ "import": "./src/McpUrlBuilder/utils/getMcpUrl.ts", "types": "./src/McpUrlBuilder/utils/getMcpUrl.ts" }, + "./MetricCard": { + "import": "./src/MetricCard/index.tsx", + "types": "./src/MetricCard/index.tsx" + }, "./MobileSheetNav/MobileSheetNav": { "import": "./src/MobileSheetNav/MobileSheetNav.tsx", "types": "./src/MobileSheetNav/MobileSheetNav.tsx" @@ -462,6 +486,10 @@ "import": "./src/PromoToast/index.ts", "types": "./src/PromoToast/index.ts" }, + "./PromoToast/styles.css": { + "import": "./src/PromoToast/styles.css", + "types": "./src/PromoToast/styles.css" + }, "./Row/Row.utils": { "import": "./src/Row/Row.utils.ts", "types": "./src/Row/Row.utils.ts" @@ -470,6 +498,10 @@ "import": "./src/Row/index.tsx", "types": "./src/Row/index.tsx" }, + "./ShimmeringLoader/index.css": { + "import": "./src/ShimmeringLoader/index.css", + "types": "./src/ShimmeringLoader/index.css" + }, "./ShimmeringLoader": { "import": "./src/ShimmeringLoader/index.tsx", "types": "./src/ShimmeringLoader/index.tsx" @@ -570,6 +602,10 @@ "import": "./src/form/Layout/FormLayout.tsx", "types": "./src/form/Layout/FormLayout.tsx" }, + "./form/Layout/InputIconContainer.module.css": { + "import": "./src/form/Layout/InputIconContainer.module.css", + "types": "./src/form/Layout/InputIconContainer.module.css" + }, "./form/Layout/InputIconContainer": { "import": "./src/form/Layout/InputIconContainer.tsx", "types": "./src/form/Layout/InputIconContainer.tsx" @@ -597,14 +633,6 @@ "./types/assets.d": { "import": "./src/types/assets.d.ts", "types": "./src/types/assets.d.ts" - }, - "./MetricCard": { - "import": "./src/MetricCard/index.tsx", - "types": "./src/MetricCard/index.tsx" - }, - "./Chart": { - "import": "./src/Chart/index.tsx", - "types": "./src/Chart/index.tsx" } }, "dependencies": { @@ -679,4 +707,4 @@ "peerDependencies": { "next": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/ui-patterns/scripts/update-exports.ts b/packages/ui-patterns/scripts/update-exports.ts index 88111ea4c3037..0ce204860fe19 100644 --- a/packages/ui-patterns/scripts/update-exports.ts +++ b/packages/ui-patterns/scripts/update-exports.ts @@ -19,7 +19,7 @@ function getAllSourceFiles(dir: string): ExportMap { if (entry.isDirectory()) { Object.assign(exportsMap, getAllSourceFiles(fullPath)) - } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) { + } else if (entry.isFile() && /\.(ts|tsx|css)$/.test(entry.name)) { const relativePath = path.relative(SRC_DIR, fullPath) const noExtension = relativePath.replace(/\.(ts|tsx)$/, '') const segments = noExtension.split(path.sep) diff --git a/packages/ui-patterns/src/InnerSideMenu/index.tsx b/packages/ui-patterns/src/InnerSideMenu/index.tsx index 14bc602ed6df9..ca609fc2bb423 100644 --- a/packages/ui-patterns/src/InnerSideMenu/index.tsx +++ b/packages/ui-patterns/src/InnerSideMenu/index.tsx @@ -18,7 +18,7 @@ import { TreeViewItemVariant, cn, } from 'ui' -import ShimmeringLoader from '../ShimmeringLoader' +import { ShimmeringLoader } from '../ShimmeringLoader' const InnerSideBarTitle = forwardRef>( (props, ref) => { diff --git a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx index 2d50ff2c78deb..564d9f791b869 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx @@ -281,6 +281,11 @@ export const MCP_CLIENTS: McpClient[] = [ }, } }, + primaryInstructions: (config, onCopy) => ( +

+ Ensure you are running Windsurf version 0.1.37 or higher. +

+ ), alternateInstructions: (config, onCopy) => (

Windsurf does not currently support remote MCP servers over HTTP transport. You need to use diff --git a/packages/ui-patterns/src/ShimmeringLoader/index.css b/packages/ui-patterns/src/ShimmeringLoader/index.css new file mode 100644 index 0000000000000..e5620e6beacaf --- /dev/null +++ b/packages/ui-patterns/src/ShimmeringLoader/index.css @@ -0,0 +1,31 @@ +/* Loaders */ +.shimmering-loader { + animation: shimmer 2s infinite linear; + background: linear-gradient( + to right, + hsl(var(--border-default)) 4%, + hsl(var(--background-surface-200)) 25%, + hsl(var(--border-default)) 36% + ); + background-size: 1000px 100%; +} + +.dark .shimmering-loader { + animation: shimmer 2s infinite linear; + background: linear-gradient( + to right, + hsl(var(--border-default)) 4%, + hsl(var(--border-control)) 25%, + hsl(var(--border-default)) 36% + ); + background-size: 1000px 100%; +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} diff --git a/packages/ui/src/components/shadcn/ui/alert-dialog.tsx b/packages/ui/src/components/shadcn/ui/alert-dialog.tsx index 80997e3820c48..095d30c247554 100644 --- a/packages/ui/src/components/shadcn/ui/alert-dialog.tsx +++ b/packages/ui/src/components/shadcn/ui/alert-dialog.tsx @@ -3,10 +3,9 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' import * as React from 'react' +import { cva, VariantProps } from 'class-variance-authority' import { cn } from '../../../lib/utils/cn' -import { buttonVariants } from './../../Button' -import { VariantProps } from 'class-variance-authority' -import { cva } from 'class-variance-authority' +import { ButtonVariantProps, buttonVariants } from './../../Button' const AlertDialog = AlertDialogPrimitive.Root @@ -61,7 +60,7 @@ const AlertDialogContentVariants = cva( }, }, defaultVariants: { - size: 'medium', + size: 'small', }, } ) @@ -90,7 +89,7 @@ const AlertDialogContent = React.forwardRef< AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -

+
) AlertDialogHeader.displayName = 'AlertDialogHeader' @@ -123,19 +122,29 @@ const AlertDialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )) AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName +type AlertDialogActionProps = React.ComponentPropsWithoutRef & { + variant?: NonNullable +} + const AlertDialogAction = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + AlertDialogActionProps +>(({ className, variant = 'primary', type = 'button', ...props }, ref) => ( )) diff --git a/supa-mdx-lint/Rule001HeadingCase.toml b/supa-mdx-lint/Rule001HeadingCase.toml index e1f6a201bb86c..45a7a23f43c39 100644 --- a/supa-mdx-lint/Rule001HeadingCase.toml +++ b/supa-mdx-lint/Rule001HeadingCase.toml @@ -254,6 +254,7 @@ may_uppercase = [ "WorkOS", "Wrappers", "Write-Ahead Log(s|ging)?", + "X", "Zoom", "Zoom Developers?", ]