Skip to content

Commit 4452e0a

Browse files
SaxonFdjhi
andauthored
Support form sidebar (supabase#45203)
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>
1 parent f32977f commit 4452e0a

25 files changed

Lines changed: 1107 additions & 297 deletions

apps/studio/components/interfaces/Support/AIAssistantOption.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
1010
interface AIAssistantOptionProps {
1111
projectRef?: string | null
1212
organizationSlug?: string | null
13+
onClick?: () => void
1314
}
1415

15-
export const AIAssistantOption = ({ projectRef, organizationSlug }: AIAssistantOptionProps) => {
16+
export const AIAssistantOption = ({
17+
projectRef,
18+
organizationSlug,
19+
onClick,
20+
}: AIAssistantOptionProps) => {
1621
const { mutate: sendEvent } = useSendEventMutation()
1722
const [isVisible, setIsVisible] = useState(false)
1823

@@ -32,7 +37,9 @@ export const AIAssistantOption = ({ projectRef, organizationSlug }: AIAssistantO
3237
: organizationSlug,
3338
},
3439
})
35-
}, [projectRef, organizationSlug, sendEvent])
40+
41+
onClick?.()
42+
}, [onClick, projectRef, organizationSlug, sendEvent])
3643

3744
// If no specific project selected, use the wildcard route
3845
const aiLink = `/project/${projectRef !== NO_PROJECT_MARKER ? projectRef : '_'}?sidebar=ai-assistant&slug=${organizationSlug}`
@@ -57,11 +64,22 @@ export const AIAssistantOption = ({ projectRef, organizationSlug }: AIAssistantO
5764
</p>
5865
</div>
5966
<div>
60-
<Link href={aiLink} onClick={onAiAssistantClicked}>
61-
<Button size="tiny" type="default" icon={<AiIconAnimation size={14} />}>
67+
{onClick ? (
68+
<Button
69+
size="tiny"
70+
type="default"
71+
icon={<AiIconAnimation size={14} />}
72+
onClick={onAiAssistantClicked}
73+
>
6274
Ask the Assistant
6375
</Button>
64-
</Link>
76+
) : (
77+
<Link href={aiLink} onClick={onAiAssistantClicked}>
78+
<Button size="tiny" type="default" icon={<AiIconAnimation size={14} />}>
79+
Ask the Assistant
80+
</Button>
81+
</Link>
82+
)}
6583
</div>
6684
</div>
6785
{/* Decorative background */}

apps/studio/components/interfaces/Support/AttachmentUpload.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ export function useAttachmentUpload() {
112112

113113
if (uploadedFiles.length === 0) return
114114

115-
const filenames = await uploadAttachments({ userId: profile.gotrue_id, files: uploadedFiles })
116-
const urls = await generateAttachmentURLs({ bucket: 'support-attachments', filenames })
117-
return urls
115+
try {
116+
const filenames = await uploadAttachments({ userId: profile.gotrue_id, files: uploadedFiles })
117+
const urls = await generateAttachmentURLs({ bucket: 'support-attachments', filenames })
118+
return urls
119+
} catch {
120+
// Ignore attachments upload errors, images are additional context and support can ask for more if needed
121+
return
122+
}
118123
// eslint-disable-next-line react-hooks/exhaustive-deps
119124
}, [profile, uploadedFiles])
120125

apps/studio/components/interfaces/Support/DashboardLogsToggle.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ import type { SupportFormValues } from './SupportForm.schema'
1616
interface DashboardLogsToggleProps {
1717
form: UseFormReturn<SupportFormValues>
1818
sanitizedLog: unknown[]
19+
align?: 'left' | 'right'
20+
className?: string
1921
}
2022

21-
export function DashboardLogsToggle({ form, sanitizedLog }: DashboardLogsToggleProps) {
23+
export function DashboardLogsToggle({
24+
form,
25+
sanitizedLog,
26+
align = 'left',
27+
className,
28+
}: DashboardLogsToggleProps) {
2229
const sanitizedLogJson = useMemo(() => JSON.stringify(sanitizedLog, null, 2), [sanitizedLog])
2330

2431
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
@@ -33,25 +40,34 @@ export function DashboardLogsToggle({ form, sanitizedLog }: DashboardLogsToggleP
3340
<FormItemLayout
3441
hideMessage
3542
name="attachDashboardLogs"
36-
className="px-6"
43+
className={className}
3744
layout="flex"
45+
align={align}
3846
label={
3947
<div className="flex items-center gap-x-2">
4048
<span className="text-foreground">Include dashboard activity log</span>
4149
</div>
4250
}
4351
description={
44-
<div className="flex flex-col gap-y-2">
52+
<div className="flex flex-col">
4553
<span className="text-foreground-light">
4654
Share sanitized logs of recent dashboard actions to help reproduce the issue.
4755
</span>
48-
<Collapsible_Shadcn_ open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
49-
<CollapsibleTrigger_Shadcn_ className="group flex items-center gap-x-1 text-sm text-foreground-lighter hover:text-foreground transition">
56+
<Collapsible_Shadcn_
57+
className="mt-2"
58+
open={isPreviewOpen}
59+
onOpenChange={setIsPreviewOpen}
60+
>
61+
<CollapsibleTrigger_Shadcn_
62+
className={
63+
'group flex items-center gap-x-1 group-data-open:text-foreground hover:text-foreground transition'
64+
}
65+
>
5066
<ChevronRight
5167
size={14}
52-
className="transition-transform group-data-open:rotate-90"
68+
className="transition-all group-data-open:rotate-90 text-foreground-muted -ml-1"
5369
/>
54-
<span>Preview log</span>
70+
<span className="text-sm">Preview log</span>
5571
</CollapsibleTrigger_Shadcn_>
5672
<CollapsibleContent_Shadcn_ className="mt-2">
5773
<pre className="bg-background-surface-200 border border-strong rounded-lg p-3 max-h-60 overflow-y-auto overflow-x-auto text-xs text-foreground-light whitespace-pre-wrap">

apps/studio/components/interfaces/Support/IncidentAdmonition.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { processIncidentData } from '@/data/platform/incident-status-utils'
99

1010
interface IncidentAdmonitionProps {
1111
isActive: boolean
12+
className?: string
1213
}
1314

1415
const STATUS_DESCRIPTION_SIGN_OFF = 'Follow the status page for updates.'
@@ -53,7 +54,7 @@ const getStatusDescription = (
5354
}
5455
}
5556

56-
export function IncidentAdmonition({ isActive }: IncidentAdmonitionProps) {
57+
export function IncidentAdmonition({ isActive, className }: IncidentAdmonitionProps) {
5758
const { data: allStatusPageEvents, isLoading, isError } = useIncidentStatusQuery()
5859
const { incidents = [] } = allStatusPageEvents ?? {}
5960

@@ -83,6 +84,7 @@ export function IncidentAdmonition({ isActive }: IncidentAdmonitionProps) {
8384
<Admonition
8485
type="warning"
8586
layout="horizontal"
87+
className={className}
8688
title={statusTitle}
8789
description={getStatusDescription(overallStatus, hasMultipleIncidents, allSameStatus)}
8890
actions={

apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export const LinkSupportTicketForm = ({
131131
projectRef={selectedProjectRef}
132132
subscriptionPlanId={subscriptionPlanId}
133133
category={category}
134-
showPlanExpectationInfo={false}
135134
/>
136135
)}
137136

apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx

Lines changed: 49 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,14 @@ interface ProjectAndPlanProps {
2424
projectRef: string | null
2525
category: ExtendedSupportCategories
2626
subscriptionPlanId: string | undefined
27-
showPlanExpectationInfo?: boolean
2827
}
2928

3029
export function ProjectAndPlanInfo({
3130
form,
3231
orgSlug,
3332
projectRef,
34-
category,
35-
subscriptionPlanId,
36-
showPlanExpectationInfo = true,
33+
category: _category,
34+
subscriptionPlanId: _subscriptionPlanId,
3735
}: ProjectAndPlanProps) {
3836
const hasProjectSelected = projectRef && projectRef !== NO_PROJECT_MARKER
3937

@@ -43,14 +41,6 @@ export function ProjectAndPlanInfo({
4341
<ProjectRefHighlighted projectRef={projectRef} />
4442

4543
{!hasProjectSelected && <Admonition type="default" title="No project has been selected" />}
46-
47-
{showPlanExpectationInfo &&
48-
orgSlug &&
49-
subscriptionPlanId !== 'enterprise' &&
50-
subscriptionPlanId !== 'platform' &&
51-
category !== 'Login_issues' && (
52-
<PlanExpectationInfoBox orgSlug={orgSlug} planId={subscriptionPlanId} />
53-
)}
5444
</div>
5545
)
5646
}
@@ -163,66 +153,60 @@ function ProjectRefHighlighted({ projectRef }: ProjectRefHighlightedProps) {
163153
)
164154
}
165155

166-
interface PlanExpectationInfoBoxProps {
156+
interface PlanExpectationInfoContentProps {
167157
orgSlug: string
168158
planId?: string
169159
}
170160

171-
const PlanExpectationInfoBox = ({ orgSlug, planId }: PlanExpectationInfoBoxProps) => {
161+
export const PlanExpectationInfoContent = ({
162+
orgSlug,
163+
planId,
164+
}: PlanExpectationInfoContentProps) => {
172165
const { billingAll } = useIsFeatureEnabled(['billing:all'])
173166
const shouldShowUpgradeActions = billingAll && planId !== 'enterprise'
174167

175168
return (
176-
<Admonition
177-
type="default"
178-
title="Expected response times are based on your organization’s plan"
179-
description={
180-
<>
181-
{planId === 'free' && (
182-
<p>
183-
Support on the Free plan is provided through the community and by the team on a
184-
best-effort basis. For a guaranteed response time, we recommend upgrading to the Pro
185-
plan. Enhanced support SLAs are available on the Enterprise plan.
186-
</p>
187-
)}
188-
189-
{planId === 'pro' && (
190-
<p>
191-
The Pro plan includes email support. In most cases, you can expect a response within 1
192-
business day for all severities. For prioritized ticketing on all issues and
193-
prioritized escalation to product engineering, we recommend upgrading to the Team
194-
plan. Enhanced support SLAs are available on the Enterprise plan.
195-
</p>
196-
)}
197-
198-
{planId === 'team' && (
199-
<p>
200-
The Team plan includes email support with prioritized ticketing and escalation to
201-
product engineering. Low, normal, and high-severity tickets are typically handled
202-
within 1 business day. Urgent issues are handled within 1 day, 365 days a year.
203-
Enhanced support SLAs are available on the Enterprise plan.
204-
</p>
205-
)}
206-
</>
207-
}
208-
actions={
209-
shouldShowUpgradeActions && (
210-
<>
211-
<Button asChild>
212-
<Link
213-
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=planSupportExpectationInfoBox`}
214-
>
215-
Upgrade plan
216-
</Link>
217-
</Button>
218-
<Button asChild type="default" icon={<ExternalLink />}>
219-
<Link href="https://supabase.com/contact/enterprise" target="_blank" rel="noreferrer">
220-
Enquire about Enterprise
221-
</Link>
222-
</Button>
223-
</>
224-
)
225-
}
226-
/>
169+
<div className="flex flex-col gap-y-3 text-sm text-foreground-light">
170+
{planId === 'free' && (
171+
<p>
172+
Support on the Free plan is provided through the community and by the team on a
173+
best-effort basis. For a guaranteed response time, we recommend upgrading to the Pro plan.
174+
Enhanced support SLAs are available on the Enterprise plan.
175+
</p>
176+
)}
177+
178+
{planId === 'pro' && (
179+
<p>
180+
Pro includes email support with typical 1-business-day responses; upgrade to Team for
181+
prioritized ticketing and engineering escalation, or Enterprise for enhanced SLAs.
182+
</p>
183+
)}
184+
185+
{planId === 'team' && (
186+
<p>
187+
The Team plan includes email support with prioritized ticketing and escalation to product
188+
engineering. Low, normal, and high-severity tickets are typically handled within 1
189+
business day. Urgent issues are handled within 1 day, 365 days a year. Enhanced support
190+
SLAs are available on the Enterprise plan.
191+
</p>
192+
)}
193+
194+
{shouldShowUpgradeActions && (
195+
<div className="flex flex-wrap gap-2 pt-1">
196+
<Button asChild size="tiny">
197+
<Link
198+
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=planSupportExpectationInfoBox`}
199+
>
200+
Upgrade plan
201+
</Link>
202+
</Button>
203+
<Button asChild type="default" size="tiny" icon={<ExternalLink />}>
204+
<Link href="https://supabase.com/contact/enterprise" target="_blank" rel="noreferrer">
205+
Enquire about Enterprise
206+
</Link>
207+
</Button>
208+
</div>
209+
)}
210+
</div>
227211
)
228212
}

apps/studio/components/interfaces/Support/SubmitButton.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import type { MouseEventHandler } from 'react'
22
// End of third-party imports
33

4-
import { Button } from 'ui'
4+
import { Button, cn } from 'ui'
55

66
interface SubmitButtonProps {
77
isSubmitting: boolean
88
userEmail: string
99
onClick?: MouseEventHandler<HTMLButtonElement>
10+
className?: string
11+
descriptionClassName?: string
1012
}
1113

12-
export function SubmitButton({ isSubmitting, userEmail, onClick }: SubmitButtonProps) {
14+
export function SubmitButton({
15+
isSubmitting,
16+
userEmail,
17+
onClick,
18+
className,
19+
descriptionClassName,
20+
}: SubmitButtonProps) {
1321
return (
14-
<div className="flex flex-col gap-3">
22+
<div className={cn('flex flex-col gap-3', className)}>
1523
<Button
1624
htmlType="submit"
1725
size="small"
@@ -22,7 +30,7 @@ export function SubmitButton({ isSubmitting, userEmail, onClick }: SubmitButtonP
2230
>
2331
Send support request
2432
</Button>
25-
<p className="text-xs text-foreground-lighter text-balance pr-4">
33+
<p className={cn('text-xs text-foreground-lighter text-balance pr-4', descriptionClassName)}>
2634
We will contact you at <span className="text-foreground font-medium">{userEmail}</span>.
2735
Please ensure emails from supabase.com are allowed.
2836
</p>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { screen } from '@testing-library/react'
2+
import userEvent from '@testing-library/user-event'
3+
import { describe, expect, it, vi } from 'vitest'
4+
5+
import { Success } from './Success'
6+
import { customRender } from '@/tests/lib/custom-render'
7+
8+
describe('Success', () => {
9+
it('renders a local finish action when provided', async () => {
10+
const onFinish = vi.fn()
11+
12+
customRender(<Success onFinish={onFinish} finishLabel="Done" />)
13+
14+
expect(screen.queryByRole('link', { name: 'Done' })).not.toBeInTheDocument()
15+
16+
await userEvent.click(screen.getByRole('button', { name: 'Done' }))
17+
18+
expect(onFinish).toHaveBeenCalledTimes(1)
19+
})
20+
})

0 commit comments

Comments
 (0)