mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
## Context This is largely just a mechanical change here: Opting to use lucide icons with the `Icon` suffix across all icon imports within `studio` - so that it’s clearer which components are icons + reduces the possibility of name collision with our own components - e.g <User /> vs <UserIcon /> Also added a eslint rule for this: <img width="1069" height="167" alt="image" src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc" /> And updated `AGENTS.md` as such: ``` Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced). ```
276 lines
9.1 KiB
TypeScript
276 lines
9.1 KiB
TypeScript
import * as Sentry from '@sentry/nextjs'
|
|
import { Loader2Icon, WrenchIcon } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useCallback, useReducer, type Dispatch, type PropsWithChildren } from 'react'
|
|
import type { UseFormReturn } from 'react-hook-form'
|
|
import SVG from 'react-inlinesvg'
|
|
import { toast } from 'sonner'
|
|
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
|
|
import { AIAssistantOption } from './AIAssistantOption'
|
|
import { DiscordCTACard } from './DiscordCTACard'
|
|
import { IncidentAdmonition } from './IncidentAdmonition'
|
|
import { Success } from './Success'
|
|
import type { ExtendedSupportCategories } from './Support.constants'
|
|
import type { SupportFormValues } from './SupportForm.schema'
|
|
import {
|
|
createInitialSupportFormState,
|
|
supportFormReducer,
|
|
type SupportFormActions,
|
|
type SupportFormState,
|
|
} from './SupportForm.state'
|
|
import { NO_PROJECT_MARKER } from './SupportForm.utils'
|
|
import { SupportFormV2 } from './SupportFormV2'
|
|
import { DEFAULT_STATUS_PAGE_URL, getSupportStatusLabel } from './SupportStatus.utils'
|
|
import { useSupportForm } from './useSupportForm'
|
|
import { useSupportStatus } from './useSupportStatus'
|
|
import CopyButton from '@/components/ui/CopyButton'
|
|
import { useStateTransition } from '@/hooks/misc/useStateTransition'
|
|
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
export { SupportForm, SupportFormStatusButton } from './SupportSidebarForm'
|
|
|
|
function useSupportFormTelemetry() {
|
|
const track = useTrack()
|
|
|
|
return useCallback(
|
|
({
|
|
projectRef,
|
|
orgSlug,
|
|
category,
|
|
}: {
|
|
projectRef: string | undefined
|
|
orgSlug: string | undefined
|
|
category: ExtendedSupportCategories
|
|
}) =>
|
|
track(
|
|
'support_ticket_submitted',
|
|
{ ticketCategory: category },
|
|
{ project: projectRef, organization: orgSlug }
|
|
),
|
|
[track]
|
|
)
|
|
}
|
|
|
|
export function SupportFormPage() {
|
|
return <SupportFormPageContent />
|
|
}
|
|
|
|
function SupportFormPageContent() {
|
|
const [state, dispatch] = useReducer(supportFormReducer, undefined, createInitialSupportFormState)
|
|
const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
|
|
|
|
const supportStatus = useSupportStatus()
|
|
const hasActiveIncidents = supportStatus.status === 'success' && supportStatus.hasActiveIncidents
|
|
const admonition = supportStatus.status === 'success' ? supportStatus.admonition : null
|
|
const statusPageUrl =
|
|
supportStatus.status === 'success' ? supportStatus.pageUrl : DEFAULT_STATUS_PAGE_URL
|
|
|
|
const sendTelemetry = useSupportFormTelemetry()
|
|
useStateTransition(state, 'submitting', 'success', (_, curr) => {
|
|
toast.success('Support request sent. Thank you!')
|
|
sendTelemetry({
|
|
projectRef: curr.sentProjectRef,
|
|
orgSlug: curr.sentOrgSlug,
|
|
category: curr.sentCategory,
|
|
})
|
|
})
|
|
|
|
useStateTransition(state, 'submitting', 'error', (_, curr) => {
|
|
toast.error(`Failed to submit support ticket: ${curr.message}`)
|
|
if (curr.code !== 429) {
|
|
Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`)
|
|
}
|
|
dispatch({ type: 'RETURN_TO_EDITING' })
|
|
})
|
|
|
|
const isSuccess = state.type === 'success'
|
|
|
|
return (
|
|
<SupportFormWrapper>
|
|
<SupportFormHeader />
|
|
|
|
<IncidentAdmonition
|
|
isActive={admonition !== null}
|
|
title={admonition?.title ?? ''}
|
|
description={admonition?.description ?? ''}
|
|
statusPageUrl={statusPageUrl}
|
|
/>
|
|
|
|
{!isSuccess && !hasActiveIncidents && (
|
|
<div className="flex flex-col gap-y-4">
|
|
<AIAssistantOption projectRef={projectRef} organizationSlug={orgSlug} />
|
|
<DiscordCTACard organizationSlug={orgSlug} />
|
|
</div>
|
|
)}
|
|
|
|
<SupportFormBody
|
|
form={form}
|
|
state={state}
|
|
dispatch={dispatch}
|
|
initialError={initialError}
|
|
selectedProjectRef={projectRef}
|
|
/>
|
|
{!isSuccess && <SupportFormDirectEmailInfo projectRef={projectRef} />}
|
|
</SupportFormWrapper>
|
|
)
|
|
}
|
|
|
|
function SupportFormWrapper({ children }: PropsWithChildren) {
|
|
return (
|
|
<div className="relative overflow-y-auto overflow-x-hidden">
|
|
<div className="mx-auto my-16 max-w-2xl w-full px-4 lg:px-6">
|
|
<div className="flex flex-col gap-y-8">{children}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SupportFormHeader() {
|
|
const supportStatus = useSupportStatus()
|
|
const isLoading = supportStatus.status === 'pending'
|
|
const isIncident = supportStatus.status === 'success' && supportStatus.hasActiveIncidents
|
|
const statusPageUrl =
|
|
supportStatus.status === 'success' ? supportStatus.pageUrl : DEFAULT_STATUS_PAGE_URL
|
|
|
|
return (
|
|
<div className="flex flex-col items-start justify-between gap-y-2 sm:flex-row sm:items-center">
|
|
<div className="flex items-center space-x-3">
|
|
<SVG src={`${BASE_PATH}/img/supabase-logo.svg`} className="h-4 w-4" />
|
|
<h3 className="m-0 text-lg">Supabase support</h3>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-x-3">
|
|
<Button asChild icon={<WrenchIcon />}>
|
|
<Link
|
|
href={`${DOCS_URL}/guides/troubleshooting?products=platform`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Troubleshooting
|
|
</Link>
|
|
</Button>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
asChild
|
|
icon={
|
|
isLoading ? (
|
|
<Loader2Icon className="animate-spin" />
|
|
) : (
|
|
<div
|
|
className={cn(
|
|
'h-2 w-2 rounded-full',
|
|
isIncident ? 'bg-warning' : 'bg-brand-default'
|
|
)}
|
|
/>
|
|
)
|
|
}
|
|
>
|
|
<Link href={statusPageUrl} target="_blank" rel="noreferrer">
|
|
{getSupportStatusLabel(supportStatus)}
|
|
</Link>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center">
|
|
Check the Supabase status page
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface SupportFormDirectEmailInfoProps {
|
|
projectRef: string | null
|
|
}
|
|
|
|
function SupportFormDirectEmailInfo({ projectRef }: SupportFormDirectEmailInfoProps) {
|
|
const hasProjectRef = projectRef && projectRef !== NO_PROJECT_MARKER
|
|
|
|
return (
|
|
<Admonition
|
|
type="default"
|
|
title="Having trouble submitting the form?"
|
|
description={
|
|
<>
|
|
<p className="mb-2.5!">
|
|
Please email us directly. Include your project ID and as much information as possible.
|
|
</p>
|
|
<p className="flex items-center gap-x-1.5 flex-wrap">
|
|
Email:{' '}
|
|
<span className="inline-flex items-center gap-x-1">
|
|
<a
|
|
href={`mailto:support@supabase.com?subject=${encodeURIComponent('Support Request')}${hasProjectRef ? `${encodeURIComponent(' for Project ID: ')}${encodeURIComponent(projectRef)}` : ''}&body=${encodeURIComponent('Here is a detailed description of the problem I am experiencing and any other information that might be helpful...')}`}
|
|
className="hover:text-foreground transition-colors duration-100"
|
|
>
|
|
<code className="text-code-inline text-foreground-light! underline decoration-foreground-lighter/50 hover:decoration-foreground-lighter/80 transition-colors duration-100">
|
|
support@supabase.com
|
|
</code>
|
|
</a>
|
|
<CopyButton
|
|
variant="text"
|
|
text="support@supabase.com"
|
|
iconOnly
|
|
onClick={() => toast.success('Copied email address to clipboard')}
|
|
/>
|
|
</span>
|
|
</p>
|
|
{hasProjectRef && (
|
|
<p className="flex items-center gap-x-1.5 flex-wrap">
|
|
Project ID:{' '}
|
|
<span className="inline-flex items-center gap-x-1">
|
|
<code className="text-code-inline text-foreground-light!">{projectRef}</code>
|
|
<CopyButton
|
|
iconOnly
|
|
variant="text"
|
|
text={projectRef}
|
|
onClick={() => toast.success('Copied project ID to clipboard')}
|
|
/>
|
|
</span>
|
|
</p>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
interface SupportFromBodyProps {
|
|
form: UseFormReturn<SupportFormValues>
|
|
state: SupportFormState
|
|
dispatch: Dispatch<SupportFormActions>
|
|
initialError: string | null
|
|
selectedProjectRef: string | null
|
|
}
|
|
|
|
function SupportFormBody({
|
|
form,
|
|
state,
|
|
dispatch,
|
|
initialError,
|
|
selectedProjectRef,
|
|
}: SupportFromBodyProps) {
|
|
const isSuccess = state.type === 'success'
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'min-w-full w-full space-y-12 rounded-sm border bg-panel-body-light shadow-md py-8',
|
|
'border-default'
|
|
)}
|
|
>
|
|
{isSuccess ? (
|
|
<Success
|
|
selectedProject={selectedProjectRef ?? undefined}
|
|
sentCategory={state.sentCategory}
|
|
/>
|
|
) : (
|
|
<SupportFormV2 form={form} initialError={initialError} state={state} dispatch={dispatch} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|