ProjectAndPlanInfo.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // End of third-party imports
  2. import { useParams } from 'common'
  3. import { AnimatePresence, motion } from 'framer-motion'
  4. import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
  5. import Link from 'next/link'
  6. import type { UseFormReturn } from 'react-hook-form'
  7. import { toast } from 'sonner'
  8. import { Button, cn, CommandGroup, CommandItem, FormControl, FormField } from 'ui'
  9. import { Admonition } from 'ui-patterns'
  10. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  11. import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
  12. import type { ExtendedSupportCategories } from './Support.constants'
  13. import type { SupportFormValues } from './SupportForm.schema'
  14. import { NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
  15. import CopyButton from '@/components/ui/CopyButton'
  16. import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
  17. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  18. interface ProjectAndPlanProps {
  19. form: UseFormReturn<SupportFormValues>
  20. orgSlug: string | null
  21. projectRef: string | null
  22. category: ExtendedSupportCategories
  23. subscriptionPlanId: string | undefined
  24. }
  25. export function ProjectAndPlanInfo({
  26. form,
  27. orgSlug,
  28. projectRef,
  29. category: _category,
  30. subscriptionPlanId: _subscriptionPlanId,
  31. }: ProjectAndPlanProps) {
  32. const hasProjectSelected = projectRef && projectRef !== NO_PROJECT_MARKER
  33. return (
  34. <div className="flex flex-col gap-y-2">
  35. <ProjectSelector form={form} orgSlug={orgSlug} projectRef={projectRef} />
  36. <ProjectRefHighlighted projectRef={projectRef} />
  37. {!hasProjectSelected && (
  38. <Admonition type="default" description="No project has been selected." />
  39. )}
  40. </div>
  41. )
  42. }
  43. interface ProjectSelectorProps {
  44. form: UseFormReturn<SupportFormValues>
  45. orgSlug: string | null
  46. projectRef: string | null
  47. }
  48. function ProjectSelector({ form, orgSlug, projectRef }: ProjectSelectorProps) {
  49. const { projectRef: urlProjectRef } = useParams()
  50. return (
  51. <FormField
  52. name="projectRef"
  53. control={form.control}
  54. render={({ field }) => (
  55. <FormItemLayout hideMessage layout="vertical" label="Which project is affected?">
  56. <FormControl>
  57. <OrganizationProjectSelector
  58. key={orgSlug}
  59. sameWidthAsTrigger
  60. fetchOnMount
  61. checkPosition="left"
  62. slug={!orgSlug || orgSlug === NO_ORG_MARKER ? undefined : orgSlug}
  63. selectedRef={field.value}
  64. onInitialLoad={(projects) => {
  65. if (!urlProjectRef && (!projectRef || projectRef === NO_PROJECT_MARKER))
  66. field.onChange(projects[0]?.ref ?? NO_PROJECT_MARKER)
  67. }}
  68. onSelect={(project) => field.onChange(project.ref)}
  69. renderTrigger={({ isLoading, project, listboxId, open }) => {
  70. return (
  71. <Button
  72. block
  73. type="default"
  74. role="combobox"
  75. aria-label="Select a project"
  76. aria-expanded={open}
  77. aria-controls={listboxId}
  78. size="small"
  79. className="justify-between"
  80. iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
  81. >
  82. {!!orgSlug && isLoading ? (
  83. <ShimmeringLoader className="w-44 py-2" />
  84. ) : !field.value || field.value === NO_PROJECT_MARKER ? (
  85. 'No specific project'
  86. ) : (
  87. (project?.name ?? 'Unknown project')
  88. )}
  89. </Button>
  90. )
  91. }}
  92. renderActions={(setOpen) => (
  93. <CommandGroup>
  94. <CommandItem
  95. className="w-full gap-x-2"
  96. onSelect={() => {
  97. field.onChange(NO_PROJECT_MARKER)
  98. setOpen(false)
  99. }}
  100. >
  101. {field.value === NO_PROJECT_MARKER && <Check size={16} />}
  102. <p className={cn(field.value !== NO_PROJECT_MARKER && 'ml-6')}>
  103. No specific project
  104. </p>
  105. </CommandItem>
  106. </CommandGroup>
  107. )}
  108. />
  109. </FormControl>
  110. </FormItemLayout>
  111. )}
  112. />
  113. )
  114. }
  115. interface ProjectRefHighlightedProps {
  116. projectRef: string | null
  117. }
  118. function ProjectRefHighlighted({ projectRef }: ProjectRefHighlightedProps) {
  119. const isVisible = !!projectRef && projectRef !== NO_PROJECT_MARKER
  120. return (
  121. <AnimatePresence>
  122. {isVisible && (
  123. <motion.div
  124. initial={{ opacity: 0, height: 0 }}
  125. animate={{ opacity: 1, height: 'auto' }}
  126. exit={{ opacity: 0, height: 0 }}
  127. transition={{ duration: 0.3 }}
  128. className="flex items-center gap-x-1"
  129. >
  130. <p className="text-sm transition text-foreground-lighter">
  131. Project ID:{' '}
  132. <code className="text-code-inline text-foreground-light!">{projectRef}</code>
  133. </p>
  134. <CopyButton
  135. iconOnly
  136. type="text"
  137. text={projectRef}
  138. onClick={() => toast.success('Copied project ID to clipboard')}
  139. />
  140. </motion.div>
  141. )}
  142. </AnimatePresence>
  143. )
  144. }
  145. interface PlanExpectationInfoContentProps {
  146. orgSlug: string
  147. planId?: string
  148. }
  149. export const PlanExpectationInfoContent = ({
  150. orgSlug,
  151. planId,
  152. }: PlanExpectationInfoContentProps) => {
  153. const { billingAll } = useIsFeatureEnabled(['billing:all'])
  154. const shouldShowUpgradeActions = billingAll && planId !== 'enterprise'
  155. return (
  156. <div className="flex flex-col gap-y-3 text-sm text-foreground-light">
  157. {planId === 'free' && (
  158. <p>
  159. Support on the Free plan is provided through the community and by the team on a
  160. best-effort basis. For a guaranteed response time, we recommend upgrading to the Pro plan.
  161. Enhanced support SLAs are available on the Enterprise plan.
  162. </p>
  163. )}
  164. {planId === 'pro' && (
  165. <p>
  166. Pro includes email support with typical 1-business-day responses; upgrade to Team for
  167. prioritized ticketing and engineering escalation, or Enterprise for enhanced SLAs.
  168. </p>
  169. )}
  170. {planId === 'team' && (
  171. <p>
  172. The Team plan includes email support with prioritized ticketing and escalation to product
  173. engineering. Low, normal, and high-severity tickets are typically handled within 1
  174. business day. Urgent issues are handled within 1 day, 365 days a year. Enhanced support
  175. SLAs are available on the Enterprise plan.
  176. </p>
  177. )}
  178. {shouldShowUpgradeActions && (
  179. <div className="flex flex-wrap gap-2 pt-1">
  180. <Button asChild size="tiny">
  181. <Link
  182. href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=planSupportExpectationInfoBox`}
  183. >
  184. Upgrade plan
  185. </Link>
  186. </Button>
  187. <Button asChild type="default" size="tiny" icon={<ExternalLink />}>
  188. <Link href="https://supabase.com/contact/enterprise" target="_blank" rel="noreferrer">
  189. Enquire about Enterprise
  190. </Link>
  191. </Button>
  192. </div>
  193. )}
  194. </div>
  195. )
  196. }