diff --git a/apps/studio/components/interfaces/Support/DisabledStateForFreeTier.tsx b/apps/studio/components/interfaces/Support/DisabledStateForFreeTier.tsx index 3b8b578605d..5b6c4a78fbb 100644 --- a/apps/studio/components/interfaces/Support/DisabledStateForFreeTier.tsx +++ b/apps/studio/components/interfaces/Support/DisabledStateForFreeTier.tsx @@ -1,5 +1,6 @@ +import { AlertCircle, ExternalLink } from 'lucide-react' import Link from 'next/link' -import { Button, IconAlertCircle, IconExternalLink } from 'ui' +import { Button } from 'ui' import InformationBox from 'components/ui/InformationBox' @@ -16,19 +17,19 @@ const DisabledStateForFreeTier = ({
} + defaultVisibility + icon={} title={`Support for ${category} is only available on the Pro Plan`} description={
-

Upgrade your project to the Pro Plan for support in this area

+

Upgrade your organization to the Pro Plan for support in this area

- +

+ + + ) +} diff --git a/apps/studio/components/interfaces/Support/LibrarySuggestions.tsx b/apps/studio/components/interfaces/Support/LibrarySuggestions.tsx new file mode 100644 index 00000000000..8bdea1ac3f4 --- /dev/null +++ b/apps/studio/components/interfaces/Support/LibrarySuggestions.tsx @@ -0,0 +1,68 @@ +import { CLIENT_LIBRARIES } from 'common' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' + +import { Button } from 'ui' + +interface LibrarySuggestionsProps { + library: string +} + +export const LibrarySuggestions = ({ library }: LibrarySuggestionsProps) => { + const selectedLibrary = CLIENT_LIBRARIES.find((lib) => lib.language === library) + const selectedClientLibraries = selectedLibrary?.libraries.filter((library) => + library.name.includes('supabase-') + ) + return ( +
+
+

+ Found an issue or a bug? Try searching our Github issues or submit a new one. +

+
+
+ {selectedClientLibraries?.map((lib) => { + const libraryLanguage = library === 'Dart (Flutter)' ? lib.name.split('-')[1] : library + return ( +
+
+

{lib.name}

+

+ For issues regarding the {libraryLanguage} client library +

+
+
+ +
+
+ ) + })} +
+
+

supabase

+

For any issues about our API

+
+
+ +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx b/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx new file mode 100644 index 00000000000..994c70f5c16 --- /dev/null +++ b/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx @@ -0,0 +1,76 @@ +import InformationBox from 'components/ui/InformationBox' +import { AlertCircle, ExternalLink } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' + +interface PlanExpectationInfoBoxProps { + orgSlug: string + projectRef: string + planId?: string +} + +export const PlanExpectationInfoBox = ({ + orgSlug, + projectRef, + planId, +}: PlanExpectationInfoBoxProps) => { + return ( + } + defaultVisibility={true} + hideCollapse={true} + title={ + projectRef === 'no-project' + ? 'Please note that no project has been selected' + : "Expected response times are based on your organization's plan" + } + {...(projectRef !== 'no-project' && { + description: ( +
+ {planId === 'free' && ( +

+ Free Plan support is available within the community and officially by the team on a + best efforts basis. For a guaranteed response we recommend upgrading to the Pro + Plan. Enhanced SLAs for support are available on our Enterprise Plan. +

+ )} + + {planId === 'pro' && ( +

+ Pro Plan includes email-based support. You can expect an answer within 1 business + day in most situations for all severities. We recommend upgrading to the Team Plan + for prioritized ticketing on all issues and prioritized escalation to product + engineering teams. Enhanced SLAs for support are available on our Enterprise Plan. +

+ )} + + {planId === 'team' && ( +

+ Team Plan includes email-based support. You get prioritized ticketing on all issues + and prioritized escalation to product engineering teams. Low, Normal, and High + severity tickets will generally be handled within 1 business day, while Urgent + issues, we respond within 1 day, 365 days a year. Enhanced SLAs for support are + available on our Enterprise Plan. +

+ )} + +
+ + +
+
+ ), + })} + /> + ) +} diff --git a/apps/studio/components/interfaces/Support/Success.tsx b/apps/studio/components/interfaces/Support/Success.tsx index 1b3ebf4b43b..5de56c89b4d 100644 --- a/apps/studio/components/interfaces/Support/Success.tsx +++ b/apps/studio/components/interfaces/Support/Success.tsx @@ -1,9 +1,10 @@ +import { Check, ExternalLink, Mail, Search } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' -import { Button, IconCheck, IconExternalLink, IconMail, IconSearch, Input, Separator } from 'ui' -import { CATEGORY_OPTIONS } from './Support.constants' import { useProfile } from 'lib/profile' +import { Button, Input, Separator } from 'ui' +import { CATEGORY_OPTIONS } from './Support.constants' interface SuccessProps { sentCategory?: string @@ -30,12 +31,12 @@ const Success = ({ return (
- +
- +
-
+

Support request successfully sent!

We will reach out to you at {respondToEmail}. @@ -64,14 +65,14 @@ const Success = ({ setSearchValue(e.target.value)} - icon={} + icon={} actions={[ - -

-
- ), - })} - /> -
- )} - - {values.category !== 'Login_issues' && ( -
-
- - {CATEGORY_OPTIONS.map((option, i) => { - return ( - - {option.label} - {option.description} - - ) - })} - - - {SEVERITY_OPTIONS.map((option: any) => { - return ( - - {option.label} - {option.description} - - ) - })} - -
- {(values.severity === 'Urgent' || values.severity === 'High') && ( -

- We do our best to respond to everyone as quickly as possible; however, - prioritization will be based on production status. We ask that you reserve High - and Urgent severity for production-impacting issues only. -

- )} -
- )} - - - - {!isDisabled ? ( - <> - {['Performance'].includes(values.category) && isFreeProject ? ( - - ) : ( - <> -
- setSubject(e.target.value)} - descriptionText={ - values.subject.length > 0 && - INCLUDE_DISCUSSIONS.includes(values.category) ? ( -

- Check our - - Github discussions - - - for a quick answer -

- ) : null - } - /> -
- - {docsResults.length > 0 && hasResults && ( -
-

- Suggested resources -

- {docsResults.slice(0, 5).map((page, i) => ( - - ))} -
- )} - - {values.category === 'Problem' && ( -
- - - Please select a library - - {CLIENT_LIBRARIES.map((option, i) => { - return ( - - {option.language} - - ) - })} - -
- )} - - {selectedLibrary !== undefined && ( -
-
-

- Found an issue or a bug? Try searching our Github issues or submit a new - one. -

-
-
- {selectedClientLibraries?.map((library) => { - const libraryLanguage = - values.library === 'Dart (Flutter)' - ? library.name.split('-')[1] - : values.library - return ( -
-
-

{library.name}

-

- For issues regarding the {libraryLanguage} client library -

-
-
- -
-
- ) - })} -
-
-

supabase

-

- For any issues about our API -

-
-
- -
-
-
-
- )} - - {values.category !== 'Login_issues' && ( -
-

- Which services are affected? -

- -
- )} -
- handleTextMessageChange(e)} - /> - {ipv4MigrationStringMatched && ( - - - Connection issues? - -

- Having trouble connecting to your project? It could be related to our - migration from PGBouncer and IPv4. -

-

- Please review this GitHub discussion. It's up to date and covers many - frequently asked questions. -

-

- -

-
-
- )} -
- {['Problem', 'Database_unresponsive', 'Performance'].includes( - values.category - ) && ( -
- -
- )} -
-
-

Attachments

-

- Upload up to {MAX_ATTACHMENTS} screenshots that might be relevant to the - issue that you're facing -

-
-
- -
-
- {uploadedDataUrls.map((x: any, idx: number) => ( -
-
removeUploadedFile(idx)} - > - -
-
- ))} - {uploadedFiles.length < MAX_ATTACHMENTS && ( -
{ - if (uploadButtonRef.current) (uploadButtonRef.current as any).click() - }} - > - -
- )} -
-
-
-
-

We will contact you at

-

{respondToEmail}

-
-
-

- Please ensure you haven't blocked Hubspot in your emails -

-
-
- -
-
- - )} - - ) : ( - <> - )} -
- ) - }} - - ) -} - -export default SupportForm - -export function getPageIcon(page: Page) { - switch (page.type) { - case PageType.Markdown: - case PageType.Reference: - case PageType.Integration: - return - case PageType.GithubDiscussion: - return - default: - throw new Error(`Unknown page type '${page.type}'`) - } -} - -export function getPageSectionIcon(page: Page) { - switch (page.type) { - case PageType.Markdown: - case PageType.Reference: - case PageType.Integration: - return - case PageType.GithubDiscussion: - return - default: - throw new Error(`Unknown page type '${page.type}'`) - } -} diff --git a/apps/studio/components/interfaces/Support/SupportForm.utils.ts b/apps/studio/components/interfaces/Support/SupportForm.utils.ts deleted file mode 100644 index cc080f2ce22..00000000000 --- a/apps/studio/components/interfaces/Support/SupportForm.utils.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { compact } from 'lodash' -import { createClient } from '@supabase/supabase-js' -import { uuidv4 } from 'lib/helpers' - -const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || '' -const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || '' - -export const uploadAttachments = async (ref: string, files: File[]) => { - const supportSupabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, { - auth: { - persistSession: false, - autoRefreshToken: false, - // @ts-ignore - multiTab: false, - detectSessionInUrl: false, - localStorage: { - getItem: (key: string) => undefined, - setItem: (key: string, value: string) => {}, - removeItem: (key: string) => {}, - }, - }, - }) - - const filesToUpload = Array.from(files) - const uploadedFiles = await Promise.all( - filesToUpload.map(async (file) => { - const suffix = file.type.split('/')[1] - const prefix = `${ref}/${uuidv4()}.${suffix}` - const options = { cacheControl: '3600' } - - const { data, error } = await supportSupabaseClient.storage - .from('support-attachments') - .upload(prefix, file, options) - - if (error) console.error('Failed to upload:', file.name, error) - return data - }) - ) - const keys = compact(uploadedFiles).map((file) => file.path) - - const { data, error } = await supportSupabaseClient.storage - .from('support-attachments') - .createSignedUrls(keys, 10 * 365 * 24 * 60 * 60) - if (error) { - console.error('Failed to retrieve URLs for attachments', error) - } - return data ? data.map((file) => file.signedUrl) : [] -} - -export const formatMessage = (message: string, attachments: string[]) => { - if (attachments.length > 0) { - const attachmentsImg = attachments.map((url) => `\n${url}`) - return `${message}\n${attachmentsImg.join('')}` - } else { - return message - } -} diff --git a/apps/studio/components/interfaces/Support/SupportForm.utils.tsx b/apps/studio/components/interfaces/Support/SupportForm.utils.tsx new file mode 100644 index 00000000000..c0e6c49b22b --- /dev/null +++ b/apps/studio/components/interfaces/Support/SupportForm.utils.tsx @@ -0,0 +1,120 @@ +import { createClient } from '@supabase/supabase-js' +import { compact } from 'lodash' +import { Book, Github, Hash, MessageSquare } from 'lucide-react' + +import { + DocsSearchResultType as PageType, + type DocsSearchResult as Page, + type DocsSearchResultSection as PageSection, +} from 'common' +import { uuidv4 } from 'lib/helpers' + +const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || '' +const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || '' + +export const uploadAttachments = async (ref: string, files: File[]) => { + const supportSupabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, { + auth: { + persistSession: false, + autoRefreshToken: false, + // @ts-ignore + multiTab: false, + detectSessionInUrl: false, + localStorage: { + getItem: (key: string) => undefined, + setItem: (key: string, value: string) => {}, + removeItem: (key: string) => {}, + }, + }, + }) + + const filesToUpload = Array.from(files) + const uploadedFiles = await Promise.all( + filesToUpload.map(async (file) => { + const suffix = file.type.split('/')[1] + const prefix = `${ref}/${uuidv4()}.${suffix}` + const options = { cacheControl: '3600' } + + const { data, error } = await supportSupabaseClient.storage + .from('support-attachments') + .upload(prefix, file, options) + + if (error) console.error('Failed to upload:', file.name, error) + return data + }) + ) + const keys = compact(uploadedFiles).map((file) => file.path) + + if (keys.length === 0) return [] + + const { data, error } = await supportSupabaseClient.storage + .from('support-attachments') + .createSignedUrls(keys, 10 * 365 * 24 * 60 * 60) + if (error) { + console.error('Failed to retrieve URLs for attachments', error) + } + return data ? data.map((file) => file.signedUrl) : [] +} + +export const formatMessage = (message: string, attachments: string[]) => { + if (attachments.length > 0) { + const attachmentsImg = attachments.map((url) => `\n${url}`) + return `${message}\n${attachmentsImg.join('')}` + } else { + return message + } +} + +export function getPageIcon(page: Page) { + switch (page.type) { + case PageType.Markdown: + case PageType.Reference: + case PageType.Integration: + return + case PageType.GithubDiscussion: + return + default: + throw new Error(`Unknown page type '${page.type}'`) + } +} + +export function getPageSectionIcon(page: Page) { + switch (page.type) { + case PageType.Markdown: + case PageType.Reference: + case PageType.Integration: + return + case PageType.GithubDiscussion: + return + default: + throw new Error(`Unknown page type '${page.type}'`) + } +} + +export function generateLink(pageType: PageType, link: string): string { + switch (pageType) { + case PageType.Markdown: + case PageType.Reference: + return `https://supabase.com/docs${link}` + case PageType.Integration: + return `https://supabase.com${link}` + case PageType.GithubDiscussion: + return link + default: + throw new Error(`Unknown page type '${pageType}'`) + } +} + +export function formatSectionUrl(page: Page, section: PageSection): string { + switch (page.type) { + case PageType.Markdown: + case PageType.GithubDiscussion: + return `${generateLink(page.type, page.path)}#${section.slug ?? ''}` + case PageType.Reference: + return `${generateLink(page.type, page.path)}/${section.slug ?? ''}` + case PageType.Integration: + return generateLink(page.type, page.path) // Assuming no section slug for Integration pages + default: + throw new Error(`Unknown page type '${page.type}'`) + } +} diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx new file mode 100644 index 00000000000..2227a81094a --- /dev/null +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -0,0 +1,719 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import * as Sentry from '@sentry/nextjs' +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { ExternalLink, Loader2, Mail, Plus, X } from 'lucide-react' +import Link from 'next/link' +import { ChangeEvent, useEffect, useRef, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { useDocsSearch, useParams, type DocsSearchResult as Page } from 'common' +import { CLIENT_LIBRARIES } from 'common/constants' +import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import type { Project } from 'data/projects/project-detail-query' +import { useProjectsQuery } from 'data/projects/projects-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useFlag } from 'hooks/ui/useFlag' +import { useProfile } from 'lib/profile' +import { + Badge, + Button, + Checkbox_Shadcn_, + cn, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + ScrollArea, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectGroup_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Separator, + TextArea_Shadcn_, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import DisabledStateForFreeTier from './DisabledStateForFreeTier' +import { DocsLinkGroup } from './DocsLink' +import { PlanExpectationInfoBox } from './PlanExpectationInfoBox' +import { + CATEGORY_OPTIONS, + IPV4_MIGRATION_STRINGS, + SERVICE_OPTIONS, + SEVERITY_OPTIONS, +} from './Support.constants' +import { LibrarySuggestions } from './LibrarySuggestions' +import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2' +import { IPV4SuggestionAlert } from './IPV4SuggestionAlert' +import { formatMessage, uploadAttachments } from './SupportForm.utils' +import { detectBrowser } from 'lib/helpers' +import { getProjectAuthConfig } from 'data/auth/auth-config-query' + +const MAX_ATTACHMENTS = 5 +const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive'] +const CONTAINER_CLASSES = 'px-6' + +interface SupportFormV2Props { + setSentCategory: (value: string) => void + setSelectedProject: (value: string) => void +} + +// [Joshen] Just naming it as V2 for now for PR review purposes so its easier to view +// This is a rewrite of the old SupportForm to use the new form components +export const SupportFormV2 = ({ setSentCategory, setSelectedProject }: SupportFormV2Props) => { + const { profile } = useProfile() + const supabaseClient = useSupabaseClient() + const { ref, slug, category: urlCategory, subject: urlSubject, message: urlMessage } = useParams() + const enableFreeSupport = useFlag('enableFreeSupport') + + const uploadButtonRef = useRef(null) + const [isSubmitting, setIsSubmitting] = useState(false) + const [docsResults, setDocsResults] = useState([]) + const [uploadedFiles, setUploadedFiles] = useState([]) + const [uploadedDataUrls, setUploadedDataUrls] = useState([]) + + const FormSchema = z + .object({ + organizationSlug: z.string().min(1, 'Please select an organization'), + projectRef: z.string().min(1, 'Please select a project'), + category: z.string(), + severity: z.string(), + library: z.string(), + subject: z.string().min(1, 'Please add a subject heading'), + message: z.string().min(1, "Please add a message about the issue that you're facing"), + affectedServices: z.string(), + allowSupportAccess: z.boolean(), + }) + .refine( + (data) => { + return !(data.category === 'Problem' && data.library === '') + }, + { + message: "Please select the library that you're facing issues with", + path: ['library'], + } + ) + + const defaultValues = { + organizationSlug: '', + projectRef: 'no-project', + category: CATEGORY_OPTIONS[0].value, + severity: 'Low', + library: '', + subject: '', + message: '', + affectedServices: '', + allowSupportAccess: false, + } + + const form = useForm>({ + mode: 'onBlur', + reValidateMode: 'onBlur', + resolver: zodResolver(FormSchema), + defaultValues, + }) + + const { organizationSlug, projectRef, category, severity, subject, library } = form.watch() + + const { + handleDocsSearchDebounced, + searchState, + searchState: state, + } = useDocsSearch(supabaseClient) + + const { + data: organizations, + isLoading: isLoadingOrganizations, + isSuccess: isSuccessOrganizations, + } = useOrganizationsQuery() + + const { + data: subscription, + isLoading: isLoadingSubscription, + isSuccess: isSuccessSubscription, + } = useOrgSubscriptionQuery({ + orgSlug: organizationSlug, + }) + + const { + data: allProjects, + isLoading: isLoadingProjects, + isSuccess: isSuccessProjects, + } = useProjectsQuery() + + const { mutate: submitSupportTicket } = useSendSupportTicketMutation({ + onSuccess: (res, variables) => { + toast.success('Support request sent. Thank you!') + setSentCategory(variables.category) + setSelectedProject(variables.projectRef ?? 'no-project') + }, + onError: (error) => { + toast.error(`Failed to submit support ticket: ${error.message}`) + Sentry.captureMessage('Failed to submit Support Form: ' + error.message) + setIsSubmitting(false) + }, + }) + + const respondToEmail = profile?.primary_email ?? 'your email' + const subscriptionPlanId = subscription?.plan.id + const projects = [ + ...(allProjects ?? []).filter((project) => project.organization_slug === organizationSlug), + { ref: 'no-project', name: 'No specific project' } as Partial, + ] + const hasResults = + state.status === 'fullResults' || + state.status === 'partialResults' || + (state.status === 'loading' && state.staleResults.length > 0) + + const onFilesUpload = async (event: ChangeEvent) => { + event.persist() + const items = event.target.files || (event as any).dataTransfer.items + const itemsCopied = Array.prototype.map.call(items, (item) => item) as File[] + const itemsToBeUploaded = itemsCopied.slice(0, MAX_ATTACHMENTS - uploadedFiles.length) + + setUploadedFiles(uploadedFiles.concat(itemsToBeUploaded)) + if (items.length + uploadedFiles.length > MAX_ATTACHMENTS) { + toast(`Only up to ${MAX_ATTACHMENTS} attachments are allowed`) + } + event.target.value = '' + } + + const removeUploadedFile = (idx: number) => { + const updatedFiles = uploadedFiles?.slice() + updatedFiles.splice(idx, 1) + setUploadedFiles(updatedFiles) + + const updatedDataUrls = uploadedDataUrls.slice() + uploadedDataUrls.splice(idx, 1) + setUploadedDataUrls(updatedDataUrls) + } + + const onSubmit: SubmitHandler> = async (values) => { + setIsSubmitting(true) + const attachments = + uploadedFiles.length > 0 ? await uploadAttachments(values.projectRef, uploadedFiles) : [] + const selectedLibrary = CLIENT_LIBRARIES.find((library) => library.language === values.library) + + const payload = { + ...values, + library: + values.category === 'Problem' && selectedLibrary !== undefined ? selectedLibrary.key : '', + message: formatMessage(values.message, attachments), + verified: true, + tags: ['dashboard-support-form'], + siteUrl: '', + additionalRedirectUrls: '', + affectedServices: values.affectedServices + .split(',') + .map((x) => x.trim().replace(/ /g, '_').toLowerCase()) + .join(';'), + browserInformation: detectBrowser(), + } + + if (values.projectRef !== 'no-project') { + try { + const authConfig = await getProjectAuthConfig({ projectRef: values.projectRef }) + payload.siteUrl = authConfig.SITE_URL + payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST + } catch (error) { + // [Joshen] No error handler required as fetching these info are nice to haves, not necessary + } + } + + submitSupportTicket(payload) + } + + useEffect(() => { + if (subject !== urlSubject && subject.trim().length > 0) { + handleDocsSearchDebounced(subject.trim()) + } else { + setDocsResults([]) + } + }, [subject, urlSubject]) + + useEffect(() => { + if (subject.trim().length > 0 && searchState.status === 'fullResults') { + setDocsResults(searchState.results) + } else if (searchState.status === 'noResults' || searchState.status === 'error') { + setDocsResults([]) + } + }, [searchState]) + + useEffect(() => { + if (!uploadedFiles) return + const objectUrls = uploadedFiles.map((file) => URL.createObjectURL(file)) + setUploadedDataUrls(objectUrls) + + return () => { + objectUrls.forEach((url: any) => URL.revokeObjectURL(url)) + } + }, [uploadedFiles]) + + useEffect(() => { + // For prefilling form fields via URL, project ref will taking higher precedence than org slug + if (isSuccessOrganizations && isSuccessProjects) { + if (ref) { + const selectedProject = allProjects.find((p) => p.ref === ref) + if (selectedProject !== undefined) { + form.setValue('organizationSlug', selectedProject.organization_slug) + form.setValue('projectRef', selectedProject.ref) + } + } else if (slug) { + const selectedOrganization = organizations?.find((org) => org.slug === slug) + if (selectedOrganization !== undefined) { + form.setValue('organizationSlug', selectedOrganization.slug) + } + } else if (ref === undefined && slug === undefined) { + form.setValue('organizationSlug', organizations[0].slug) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref, slug, isSuccessOrganizations, isSuccessProjects]) + + useEffect(() => { + if (urlCategory) { + const validCategory = CATEGORY_OPTIONS.find((option) => { + if (option.value.toLowerCase() === ((urlCategory as string) ?? '').toLowerCase()) + return option + }) + if (validCategory !== undefined) form.setValue('category', validCategory.value) + } + }, [urlCategory]) + + useEffect(() => { + if (urlSubject) form.setValue('subject', urlSubject) + }, [urlSubject]) + + useEffect(() => { + if (urlMessage) form.setValue('message', urlMessage) + }, [urlMessage]) + + return ( + +
+

How can we help?

+ + ( + + + + + +
+ {(organizations ?? []).find((o) => o.slug === field.value)?.name} + {isLoadingSubscription && } + {isSuccessSubscription && ( + + {subscriptionPlanId} + + )} +
+
+
+ + + {organizations?.map((org) => ( + {org.name} + ))} + + +
+
+
+ )} + /> + +
+ ( + + + { + if (val.length > 0) field.onChange(val) + }} + > + + + + + + {projects?.map((project) => ( + + {project.name} + + ))} + + + + + + )} + /> + {organizationSlug && + subscriptionPlanId !== 'enterprise' && + category !== 'Login_issues' && ( + + )} +
+ +
+ ( + + + + + + {CATEGORY_OPTIONS.find((o) => o.value === field.value)?.label} + + + + + {CATEGORY_OPTIONS.map((option) => ( + + {option.label} + + {option.description} + + + ))} + + + + + + )} + /> + ( + + + + + + {field.value} + + + + + {SEVERITY_OPTIONS.map((option) => ( + + {option.label} + + {option.description} + + + ))} + + + + + + )} + /> + {(severity === 'Urgent' || severity === 'High') && ( +

+ We do our best to respond to everyone as quickly as possible; however, prioritization + will be based on production status. We ask that you reserve High and Urgent severity + for production-impacting issues only. +

+ )} +
+ + + + {!enableFreeSupport || (category === 'Performance' && subscriptionPlanId === 'free') ? ( + o.value === category)?.label ?? ''} + organizationSlug={organizationSlug} + /> + ) : ( + <> +
+ ( + 0 && + INCLUDE_DISCUSSIONS.includes(category) && ( +

+ Check our + + Github discussions + + + for a quick answer +

+ ) + } + > + + + +
+ )} + /> + + {docsResults.length > 0 && hasResults && ( +
+

+ Suggested resources ({Math.min(docsResults.length, 5)}) +

+ 3 ? 'h-[300px]' : ''}> + {docsResults.slice(0, 5).map((page, i) => ( + + ))} + +
+ )} +
+ + {category === 'Problem' && ( + ( + + + + + + + + + {CLIENT_LIBRARIES.map((option) => ( + + {option.language} + + ))} + + + + + + )} + /> + )} + + {library.length > 0 && } + + {category !== 'Login_issues' && ( + ( + + + + form.setValue('affectedServices', services.join(', ')) + } + /> + + + )} + /> + )} + + ( + field.value.includes(str)) && ( + + ) + } + > + + + + + )} + /> + + {['Problem', 'Database_unresponsive', 'Performance'].includes(category) && ( + ( + + + field.onChange(value)} + /> + + + )} + /> + )} + +
+
+

Attachments

+

+ Upload up to {MAX_ATTACHMENTS} screenshots that might be relevant to the issue + that you're facing +

+
+ +
+ {uploadedDataUrls.map((x: any, idx: number) => ( +
+
removeUploadedFile(idx)} + > + +
+
+ ))} + {uploadedFiles.length < MAX_ATTACHMENTS && ( +
{ + if (uploadButtonRef.current) (uploadButtonRef.current as any).click() + }} + > + +
+ )} +
+
+ +
+
+

We will contact you at

+

{respondToEmail}

+
+
+

+ Please ensure you haven't blocked Hubspot in your emails +

+
+
+ +
+
+ + )} + +
+ ) +} diff --git a/apps/studio/components/ui/InformationBox.tsx b/apps/studio/components/ui/InformationBox.tsx index 1ea7c26661c..4bcfc2e4116 100644 --- a/apps/studio/components/ui/InformationBox.tsx +++ b/apps/studio/components/ui/InformationBox.tsx @@ -1,6 +1,8 @@ +import { ExternalLink, Maximize2, Minimize2 } from 'lucide-react' import Link from 'next/link' import { ReactNode, useState } from 'react' -import { Button, IconExternalLink, IconMaximize2, IconMinimize2 } from 'ui' + +import { Button } from 'ui' interface InformationBoxProps { icon?: ReactNode @@ -48,9 +50,9 @@ const InformationBox = ({ onClick={() => setIsExpanded(!isExpanded)} > {isExpanded ? ( - + ) : ( - + )}
) : null} @@ -66,7 +68,7 @@ const InformationBox = ({ {url && (
-
-
- - - + + - - - - -
- Check Supabase status page -
-
-
-
+ + + Check Supabase status page + +
+
{ projects={projectsData} /> ) : ( -