mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
Refactors our help sidebar within Studio to include the actual support form itself when contact is selected. This PR also cleans up the initial state of the sidebar and the options within. ## To test: - Open an org and click the help icon top right - Click contact support - Submit a support ticket - Click done to return to support sidebar state <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Support form V3 and support sidebar with status button; direct-email helper and URL prefill * Success screen supports onFinish callback and customizable finish label * AI Assistant and Help options accept optional click callbacks; resource items gain keyboard/accessibility support * **Refactor** * Help panel split into home/support views with back navigation * Support components accept flexible align/className props and layout/styling tweaks * Initial URL params loader added for support form * **Tests** * New/updated tests for support flows, success screen, and help options interactions <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
282 lines
9.2 KiB
TypeScript
282 lines
9.2 KiB
TypeScript
import * as Sentry from '@sentry/nextjs'
|
|
import { Loader2, Wrench } 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 { useSupportForm } from './useSupportForm'
|
|
import CopyButton from '@/components/ui/CopyButton'
|
|
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
|
|
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
|
import { useStateTransition } from '@/hooks/misc/useStateTransition'
|
|
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
|
|
|
export { SupportForm, SupportFormStatusButton } from './SupportSidebarForm'
|
|
|
|
function useSupportFormTelemetry() {
|
|
const { mutate: sendEvent } = useSendEventMutation()
|
|
|
|
return useCallback(
|
|
({
|
|
projectRef,
|
|
orgSlug,
|
|
category,
|
|
}: {
|
|
projectRef: string | undefined
|
|
orgSlug: string | undefined
|
|
category: ExtendedSupportCategories
|
|
}) =>
|
|
sendEvent({
|
|
action: 'support_ticket_submitted',
|
|
properties: {
|
|
ticketCategory: category,
|
|
},
|
|
groups: {
|
|
project: projectRef,
|
|
organization: orgSlug,
|
|
},
|
|
}),
|
|
[sendEvent]
|
|
)
|
|
}
|
|
|
|
export function SupportFormPage() {
|
|
return <SupportFormPageContent />
|
|
}
|
|
|
|
function SupportFormPageContent() {
|
|
const [state, dispatch] = useReducer(supportFormReducer, undefined, createInitialSupportFormState)
|
|
const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
|
|
|
|
const {
|
|
data: allStatusPageEvents,
|
|
isPending: isIncidentsPending,
|
|
isError: isIncidentsError,
|
|
} = useIncidentStatusQuery()
|
|
const { incidents = [] } = allStatusPageEvents ?? {}
|
|
const hasActiveIncidents =
|
|
!isIncidentsPending && !isIncidentsError && incidents && incidents.length > 0
|
|
|
|
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}`)
|
|
Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`)
|
|
dispatch({ type: 'RETURN_TO_EDITING' })
|
|
})
|
|
|
|
const isSuccess = state.type === 'success'
|
|
|
|
return (
|
|
<SupportFormWrapper>
|
|
<SupportFormHeader />
|
|
|
|
<IncidentAdmonition isActive={hasActiveIncidents} />
|
|
|
|
{!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 { data: allStatusPageEvents, isPending: isLoading, isError } = useIncidentStatusQuery()
|
|
const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}
|
|
const isMaintenance = maintenanceEvents.length > 0
|
|
const isIncident = incidents.length > 0
|
|
|
|
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 type="default" icon={<Wrench />}>
|
|
<Link
|
|
href={`${DOCS_URL}/guides/troubleshooting?products=platform`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Troubleshooting
|
|
</Link>
|
|
</Button>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
asChild
|
|
type="default"
|
|
icon={
|
|
isLoading ? (
|
|
<Loader2 className="animate-spin" />
|
|
) : (
|
|
<div
|
|
className={cn('h-2 w-2 rounded-full', isIncident ? 'bg-warning' : 'bg-brand')}
|
|
/>
|
|
)
|
|
}
|
|
>
|
|
<Link href="https://status.supabase.com/" target="_blank" rel="noreferrer">
|
|
{isLoading
|
|
? 'Checking status'
|
|
: isError
|
|
? 'Failed to check status'
|
|
: isIncident
|
|
? 'Active incident ongoing'
|
|
: isMaintenance
|
|
? 'Scheduled maintenance'
|
|
: 'All systems operational'}
|
|
</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
|
|
type="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
|
|
type="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',
|
|
`${isSuccess ? 'pt-8' : 'py-8'}`,
|
|
'border-default'
|
|
)}
|
|
>
|
|
{isSuccess ? (
|
|
<Success
|
|
selectedProject={selectedProjectRef ?? undefined}
|
|
sentCategory={state.sentCategory}
|
|
/>
|
|
) : (
|
|
<SupportFormV2 form={form} initialError={initialError} state={state} dispatch={dispatch} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|