mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): authentication empty states (#40883)
* auth hooks and docs * third-party auth * remove unrelated changes * remove unrelated changes * docs tighten * docs tighten * Nit clean up --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
d02926a68f
commit
080cc7aeab
15 files changed
+217
-108
No files matched your search
@@ -16,6 +16,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"EmptyStatePresentational": {
|
||||
name: "EmptyStatePresentational",
|
||||
type: "components:fragment",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/src/EmptyStatePresentational")),
|
||||
source: "",
|
||||
files: ["registry/default//EmptyStatePresentational/index.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"TextConfirmModal": {
|
||||
name: "TextConfirmModal",
|
||||
type: "components:fragment",
|
||||
@@ -2447,13 +2458,35 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state-initial-state-presentational": {
|
||||
name: "empty-state-initial-state-presentational",
|
||||
"empty-state-presentational-demo": {
|
||||
name: "empty-state-presentational-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state-presentational")),
|
||||
component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/empty-state-initial-state-presentational.tsx"],
|
||||
files: ["registry/default/example/empty-state-presentational-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state-presentational-buttons": {
|
||||
name: "empty-state-presentational-buttons",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-buttons")),
|
||||
source: "",
|
||||
files: ["registry/default/example/empty-state-presentational-buttons.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state-presentational-icon": {
|
||||
name: "empty-state-presentational-icon",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-icon")),
|
||||
source: "",
|
||||
files: ["registry/default/example/empty-state-presentational-icon.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
---
|
||||
title: EmptyStatePresentational
|
||||
description: An initial state with value propositions and actions.
|
||||
description: An empty state for encouraging action.
|
||||
component: true
|
||||
fragment: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="empty-state-initial-state-presentational" peekCode />
|
||||
<ComponentPreview name="empty-state-presentational-icon" peekCode />
|
||||
|
||||
Supports both Lucide icons and custom icon components from the `icons` package. Expects a title, description, and optional action buttons or other children.
|
||||
Expects a title, description, and optional action buttons or other children.
|
||||
|
||||
## Props
|
||||
|
||||
### Text
|
||||
|
||||
All text should be written using active language. The title should prompt the user to take an action, and the description should clearly explain the value of doing so.
|
||||
|
||||
### Icon
|
||||
|
||||
Supports both Lucide icons and [custom icons](../icons) via the `icons` package. If neither are passed, EmptyStatePresentational falls back to Lucide’s `SquarePlus`.
|
||||
|
||||
<ComponentPreview name="empty-state-presentational-demo" peekCode />
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
When repeating buttons, set the `type` to `default` so the original `primary`, button remains the only `primary` action on display.
|
||||
|
||||
<ComponentPreview name="empty-state-presentational-buttons" peekCode />
|
||||
@@ -44,7 +44,9 @@ Perhaps the user has not yet created any data. The presentation of this empty st
|
||||
|
||||
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.
|
||||
|
||||
<ComponentPreview name="empty-state-initial-state-presentational" peekCode wide />
|
||||
<ComponentPreview name="empty-state-presentational-icon" peekCode wide />
|
||||
|
||||
Remember to use active language in these cases. 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
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionAside,
|
||||
PageSectionContent,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
|
||||
export default function EmptyStatePresentationalIcon() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Providers</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<Button size="tiny" type="primary" icon={<Plus size={14} />}>
|
||||
Add provider
|
||||
</Button>
|
||||
</PageSectionAside>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<EmptyStatePresentational
|
||||
title="Add a provider"
|
||||
description="Use third-party authentication systems to access your project."
|
||||
>
|
||||
<Button size="tiny" type="default" icon={<Plus size={14} />}>
|
||||
Add provider
|
||||
</Button>
|
||||
</EmptyStatePresentational>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
|
||||
export default function EmptyStatePresentationalIcon() {
|
||||
return (
|
||||
<EmptyStatePresentational
|
||||
title="Create an auth hook"
|
||||
description="Use Postgres functions or HTTP endpoints to customize your authentication flow."
|
||||
>
|
||||
<Button size="tiny" type="primary" icon={<Plus size={14} />}>
|
||||
Add hook
|
||||
</Button>
|
||||
</EmptyStatePresentational>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Plus } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
|
||||
export default function EmptyStateInitialStatePresentational() {
|
||||
export default function EmptyStatePresentationalIcon() {
|
||||
return (
|
||||
<EmptyStatePresentational
|
||||
icon={BucketPlus}
|
||||
@@ -1319,9 +1319,19 @@ export const examples: Registry = [
|
||||
files: ['example/empty-state-zero-items-table.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'empty-state-initial-state-presentational',
|
||||
name: 'empty-state-presentational-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/empty-state-initial-state-presentational.tsx'],
|
||||
files: ['example/empty-state-presentational-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'empty-state-presentational-buttons',
|
||||
type: 'components:example',
|
||||
files: ['example/empty-state-presentational-buttons.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'empty-state-presentational-icon',
|
||||
type: 'components:example',
|
||||
files: ['example/empty-state-presentational-icon.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'empty-state-initial-state-informational',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChevronDown } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
@@ -21,12 +22,14 @@ import { extractMethod, isValidHook } from './hooks.utils'
|
||||
interface AddHookDropdownProps {
|
||||
buttonText?: string
|
||||
align?: 'end' | 'center'
|
||||
type?: 'primary' | 'default'
|
||||
onSelectHook: (hook: HOOK_DEFINITION_TITLE) => void
|
||||
}
|
||||
|
||||
export const AddHookDropdown = ({
|
||||
buttonText = 'Add hook',
|
||||
align = 'end',
|
||||
type = 'primary',
|
||||
onSelectHook,
|
||||
}: AddHookDropdownProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -56,7 +59,7 @@ export const AddHookDropdown = ({
|
||||
return (
|
||||
<ButtonTooltip
|
||||
disabled
|
||||
type="primary"
|
||||
type={type}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: 'You need additional permissions to add auth hooks' },
|
||||
}}
|
||||
@@ -69,7 +72,7 @@ export const AddHookDropdown = ({
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="primary" iconRight={<ChevronDown className="w-4 h-4" strokeWidth={1} />}>
|
||||
<Button type={type} iconRight={<ChevronDown />}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -89,14 +92,7 @@ export const AddHookDropdown = ({
|
||||
<p className="text-foreground-light">Team or Enterprise Plan required</p>
|
||||
<p className="text-foreground-lighter text-xs">
|
||||
The following hooks are not available on{' '}
|
||||
<a
|
||||
target="_href"
|
||||
href={`https://supabase.com/dashboard/org/${organization?.slug ?? '_'}/billing`}
|
||||
className="underline"
|
||||
>
|
||||
your plan
|
||||
</a>
|
||||
.
|
||||
<InlineLink href={`/org/${organization?.slug ?? '_'}/billing`}>your plan</InlineLink>.
|
||||
</p>
|
||||
</DropdownMenuLabel>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { X } from 'lucide-react'
|
||||
import randomBytes from 'randombytes'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
@@ -28,16 +26,15 @@ import {
|
||||
RadioGroupStackedItem,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
Switch,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants'
|
||||
import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils'
|
||||
|
||||
@@ -269,28 +266,21 @@ export const CreateHookSheet = ({
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authConfig, title, visible, definition])
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={() => onClose()}>
|
||||
<SheetContent size="lg" showClose={false} className="flex flex-col gap-0">
|
||||
<SheetContent
|
||||
aria-describedby={undefined}
|
||||
size="lg"
|
||||
showClose={false}
|
||||
className="flex flex-col gap-0"
|
||||
>
|
||||
<SheetHeader className="py-3 flex flex-row justify-between items-center border-b-0">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<SheetClose
|
||||
className={cn(
|
||||
'text-muted hover:text ring-offset-background transition-opacity hover:opacity-100',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'disabled:pointer-events-none data-[state=open]:bg-secondary',
|
||||
'transition'
|
||||
)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose>
|
||||
<SheetTitle className="truncate">
|
||||
{isCreating ? `Add ${title}` : `Update ${title}`}
|
||||
</SheetTitle>
|
||||
</div>
|
||||
<SheetTitle className="truncate">
|
||||
{isCreating ? `Add ${title}` : `Update ${title}`}
|
||||
</SheetTitle>
|
||||
<DocsButton href={`${DOCS_URL}/guides/auth/auth-hooks/${hook.docSlug}`} />
|
||||
</SheetHeader>
|
||||
<Separator />
|
||||
@@ -459,11 +449,17 @@ export const CreateHookSheet = ({
|
||||
<FormItemLayout
|
||||
label="Secret"
|
||||
description={
|
||||
<ReactMarkdown>
|
||||
It should be a base64 encoded hook secret with a prefix `v1,whsec_`.
|
||||
`v1` denotes the signature version, and `whsec_` signifies a symmetric
|
||||
secret.
|
||||
</ReactMarkdown>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p>
|
||||
Should be a base64 encoded hook secret with a prefix{' '}
|
||||
<code className="text-code-inline">v1,whsec_</code>.
|
||||
</p>
|
||||
<InfoTooltip side="bottom" className="w-60 text-center">
|
||||
<code className="text-code-inline">v1</code> denotes the signature
|
||||
version and <code className="text-code-inline">whsec_</code> signifies
|
||||
a symmetric secret.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
@@ -472,7 +468,7 @@ export const CreateHookSheet = ({
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="rounded-l-none"
|
||||
className="rounded-l-none text-xs"
|
||||
onClick={() => {
|
||||
const authHookSecret = generateAuthHookSecret()
|
||||
form.setValue('httpsValues.secret', authHookSecret)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { cn } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import { EmptyStatePresentational, GenericSkeletonLoader } from 'ui-patterns'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import {
|
||||
PageSection,
|
||||
@@ -109,7 +109,7 @@ export const HooksListing = () => {
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>All hooks</PageSectionTitle>
|
||||
<PageSectionTitle>Hooks</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<AddHookDropdown
|
||||
@@ -122,14 +122,12 @@ export const HooksListing = () => {
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
{hooks.filter((h) => isValidHook(h)).length === 0 && (
|
||||
<div
|
||||
className={[
|
||||
'border rounded border-default px-20 py-16',
|
||||
'flex flex-col items-center justify-center space-y-4',
|
||||
].join(' ')}
|
||||
<EmptyStatePresentational
|
||||
title="Create an auth hook"
|
||||
description="Use Postgres functions or HTTP endpoints to customize your authentication flow."
|
||||
>
|
||||
<p className="text-sm text-foreground-light">No hooks configured yet</p>
|
||||
<AddHookDropdown
|
||||
type="default"
|
||||
align="center"
|
||||
buttonText="Add a new hook"
|
||||
onSelectHook={(title) => {
|
||||
@@ -137,7 +135,7 @@ export const HooksListing = () => {
|
||||
if (hook) setSelectedHook(hook.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EmptyStatePresentational>
|
||||
)}
|
||||
|
||||
<div className="-space-y-px">
|
||||
|
||||
+4
-2
@@ -21,6 +21,7 @@ import {
|
||||
interface AddIntegrationDropdownProps {
|
||||
buttonText?: string
|
||||
align?: 'end' | 'center'
|
||||
type?: 'primary' | 'default'
|
||||
onSelectIntegrationType: (type: INTEGRATION_TYPES) => void
|
||||
}
|
||||
|
||||
@@ -47,6 +48,7 @@ const ProviderDropdownItem = ({
|
||||
}
|
||||
|
||||
export const AddIntegrationDropdown = ({
|
||||
type = 'primary',
|
||||
align = 'end',
|
||||
onSelectIntegrationType,
|
||||
}: AddIntegrationDropdownProps) => {
|
||||
@@ -55,12 +57,12 @@ export const AddIntegrationDropdown = ({
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="primary" iconRight={<ChevronDown size={14} strokeWidth={1} />}>
|
||||
<Button type={type} iconRight={<ChevronDown />}>
|
||||
Add provider
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={align} className="w-56">
|
||||
<DropdownMenuLabel>Select Provider</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Select provider</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<ProviderDropdownItem type="firebase" onSelectIntegrationType={onSelectIntegrationType} />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { cn } from 'ui'
|
||||
import { EmptyStatePresentational } from 'ui-patterns'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import {
|
||||
PageSection,
|
||||
@@ -63,7 +64,7 @@ export const ThirdPartyAuthForm = () => {
|
||||
return (
|
||||
<AlertError
|
||||
error={error}
|
||||
subject="Failed to retrieve auth configuration for Third Party Auth Integrations"
|
||||
subject="Failed to retrieve auth configuration for Third-Party Auth Integrations"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -72,27 +73,20 @@ export const ThirdPartyAuthForm = () => {
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Third Party Auth</PageSectionTitle>
|
||||
<PageSectionTitle>Third-Party Auth</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Use third-party authentication (TPA) systems based on JWTs to access your project.
|
||||
<br />
|
||||
Billing is based on the number of monthly active users (MAUs) requesting your API
|
||||
throughout the billing period. Refer to our{' '}
|
||||
throughout the billing period.{' '}
|
||||
<InlineLink
|
||||
href={`${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-third-party`}
|
||||
>
|
||||
billing docs
|
||||
</InlineLink>{' '}
|
||||
for more information.
|
||||
Learn more
|
||||
</InlineLink>
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<div className="flex items-center gap-2">
|
||||
<DocsButton href={`${DOCS_URL}/guides/auth/third-party/overview`} />
|
||||
{integrations.length !== 0 && (
|
||||
<AddIntegrationDropdown onSelectIntegrationType={setSelectedIntegration} />
|
||||
)}
|
||||
</div>
|
||||
<DocsButton href={`${DOCS_URL}/guides/auth/third-party/overview`} />
|
||||
<AddIntegrationDropdown onSelectIntegrationType={setSelectedIntegration} />
|
||||
</PageSectionAside>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
@@ -108,18 +102,16 @@ export const ThirdPartyAuthForm = () => {
|
||||
|
||||
{isSuccess ? (
|
||||
integrations.length === 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
'border rounded border-default px-20 py-16 flex flex-col items-center justify-center space-y-4'
|
||||
)}
|
||||
<EmptyStatePresentational
|
||||
title="Add an authentication provider"
|
||||
description="Use third-party authentication systems based on JWTs to access your project."
|
||||
>
|
||||
<p className="text-sm text-foreground-light">No providers configured yet</p>
|
||||
<AddIntegrationDropdown
|
||||
align="center"
|
||||
buttonText="Add a new integration"
|
||||
type="default"
|
||||
onSelectIntegrationType={setSelectedIntegration}
|
||||
/>
|
||||
</div>
|
||||
</EmptyStatePresentational>
|
||||
) : (
|
||||
<div className="-space-y-px">
|
||||
{integrations.map((integration) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const AuthProvidersLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
...(authenticationThirdPartyAuth
|
||||
? [
|
||||
{
|
||||
label: 'Third Party Auth',
|
||||
label: 'Third-Party Auth',
|
||||
href: `/project/${ref}/auth/third-party`,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -36,9 +36,7 @@ const Hooks: NextPageWithLayout = () => {
|
||||
<PageHeaderMeta>
|
||||
<PageHeaderSummary>
|
||||
<PageHeaderTitle>Auth Hooks</PageHeaderTitle>
|
||||
<PageHeaderDescription>
|
||||
Use Postgres functions or HTTP endpoints to customize the behavior of Supabase Auth
|
||||
</PageHeaderDescription>
|
||||
<PageHeaderDescription>Customize your authentication flow</PageHeaderDescription>
|
||||
</PageHeaderSummary>
|
||||
<PageHeaderAside>
|
||||
<DocsButton href={`${DOCS_URL}/guides/auth/auth-hooks`} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { SquarePlus } from 'lucide-react'
|
||||
import { createElement, isValidElement, ReactNode } from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
@@ -59,6 +60,9 @@ export const EmptyStatePresentational = ({
|
||||
iconClassName,
|
||||
contentClassName,
|
||||
}: EmptyStatePresentationalProps) => {
|
||||
// Use SquarePlus as default icon if none is provided
|
||||
const iconToRender = Icon || SquarePlus
|
||||
|
||||
// Extract content rendering to avoid duplication between icon and no-icon cases
|
||||
const textContent = (
|
||||
<div className={cn('flex flex-col items-center text-center text-balance', contentClassName)}>
|
||||
@@ -74,30 +78,28 @@ export const EmptyStatePresentational = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon ? (
|
||||
<div className="flex flex-col gap-y-3 items-center">
|
||||
{/*
|
||||
Handle different icon types:
|
||||
1. If it's already a React element (pre-rendered), render it directly
|
||||
2. If it's a function (LucideIcon component type) or forwardRef component (has $$typeof),
|
||||
instantiate it with createElement and apply default props
|
||||
3. Otherwise, render as-is (fallback for other ReactNode types)
|
||||
*/}
|
||||
{isValidElement(Icon)
|
||||
? Icon
|
||||
: typeof Icon === 'function' ||
|
||||
(typeof Icon === 'object' && Icon !== null && '$$typeof' in Icon)
|
||||
? createElement(Icon as LucideIcon, {
|
||||
size: iconSize,
|
||||
strokeWidth: 1.5,
|
||||
className: cn('text-foreground-muted', iconClassName),
|
||||
})
|
||||
: Icon}
|
||||
{textContent}
|
||||
</div>
|
||||
) : (
|
||||
textContent
|
||||
)}
|
||||
<div className="flex flex-col gap-y-3 items-center">
|
||||
{/*
|
||||
Handle different icon types:
|
||||
1. If it's already a React element (pre-rendered), render it directly
|
||||
2. If it's a function (LucideIcon component type) or forwardRef component (has $$typeof),
|
||||
instantiate it with createElement and apply default props
|
||||
3. Otherwise, render as-is (fallback for other ReactNode types)
|
||||
*/}
|
||||
{isValidElement(iconToRender)
|
||||
? iconToRender
|
||||
: typeof iconToRender === 'function' ||
|
||||
(typeof iconToRender === 'object' &&
|
||||
iconToRender !== null &&
|
||||
'$$typeof' in iconToRender)
|
||||
? createElement(iconToRender as LucideIcon, {
|
||||
size: iconSize,
|
||||
strokeWidth: 1.5,
|
||||
className: cn('text-foreground-muted', iconClassName),
|
||||
})
|
||||
: iconToRender}
|
||||
{textContent}
|
||||
</div>
|
||||
|
||||
{/* Optional children (typically action buttons) */}
|
||||
{children}
|
||||
|
||||
Reference in new issue
Block a user