ChooseChannelPopover.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. // @ts-nocheck
  2. import { zodResolver } from '@hookform/resolvers/zod'
  3. import { IS_PLATFORM, useParams } from 'common'
  4. import { ChevronDown } from 'lucide-react'
  5. import { Dispatch, SetStateAction, useState } from 'react'
  6. import { useForm } from 'react-hook-form'
  7. import {
  8. Button,
  9. Form,
  10. FormControl,
  11. FormDescription,
  12. FormField,
  13. FormItem,
  14. FormLabel,
  15. Input,
  16. Popover,
  17. PopoverContent,
  18. PopoverTrigger,
  19. Switch,
  20. } from 'ui'
  21. import * as z from 'zod'
  22. import { RealtimeConfig } from './useRealtimeMessages'
  23. import { DocsButton } from '@/components/ui/DocsButton'
  24. import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
  25. import { getTemporaryAPIKey } from '@/data/api-keys/temp-api-keys-query'
  26. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  27. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  28. import { DOCS_URL } from '@/lib/constants'
  29. import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
  30. type ControlledOpenProps =
  31. | { open: boolean; onOpenChange: (open: boolean) => void }
  32. | { open?: undefined; onOpenChange?: undefined }
  33. type ChooseChannelPopoverProps = {
  34. config: RealtimeConfig
  35. onChangeConfig: Dispatch<SetStateAction<RealtimeConfig>>
  36. } & ControlledOpenProps
  37. const FormSchema = z.object({ channel: z.string(), isPrivate: z.boolean() })
  38. export const ChooseChannelPopover = ({
  39. config,
  40. onChangeConfig,
  41. open: controlledOpen,
  42. onOpenChange,
  43. }: ChooseChannelPopoverProps) => {
  44. const [internalOpen, setInternalOpen] = useState(false)
  45. const isControlled = controlledOpen !== undefined
  46. const open = isControlled ? controlledOpen : internalOpen
  47. const setOpen = (v: boolean) => {
  48. if (isControlled) {
  49. onOpenChange?.(v)
  50. } else {
  51. setInternalOpen(v)
  52. }
  53. }
  54. const { ref } = useParams()
  55. const { data: org } = useSelectedOrganizationQuery()
  56. const { mutate: sendEvent } = useSendEventMutation()
  57. const form = useForm<z.infer<typeof FormSchema>>({
  58. mode: 'onBlur',
  59. reValidateMode: 'onBlur',
  60. resolver: zodResolver(FormSchema as any),
  61. defaultValues: { channel: '', isPrivate: false },
  62. })
  63. const onOpen = (v: boolean) => {
  64. // when opening, copy the outside config into the intermediate one
  65. if (v === true) {
  66. form.setValue('channel', config.channelName)
  67. }
  68. setOpen(v)
  69. }
  70. const onSubmit = async () => {
  71. setOpen(false)
  72. sendEvent({
  73. action: 'realtime_inspector_listen_channel_clicked',
  74. groups: {
  75. project: ref ?? 'Unknown',
  76. organization: org?.slug ?? 'Unknown',
  77. },
  78. })
  79. let token = config.token
  80. // [Joshen] Refresh if starting to listen + using temp API key, since it has a low refresh rate
  81. if (token.startsWith('sb_temp') || !IS_PLATFORM) {
  82. const data = await getTemporaryAPIKey({ projectRef: config.projectRef, expiry: 3600 })
  83. token = data.api_key
  84. }
  85. onChangeConfig({
  86. ...config,
  87. token,
  88. channelName: form.getValues('channel'),
  89. isChannelPrivate: form.getValues('isPrivate'),
  90. enabled: true,
  91. })
  92. }
  93. const channelPopoverTrigger = (
  94. <PopoverTrigger asChild>
  95. <Button className="rounded-r-none" type="default" size="tiny" iconRight={<ChevronDown />}>
  96. <p
  97. className="max-w-[120px] truncate"
  98. title={config.channelName.length > 0 ? config.channelName : ''}
  99. >
  100. {config.channelName.length > 0 ? `Channel: ${config.channelName}` : 'Join a channel'}
  101. </p>
  102. </Button>
  103. </PopoverTrigger>
  104. )
  105. return (
  106. <Popover open={open} onOpenChange={onOpen}>
  107. {!open && config.channelName.length === 0 ? (
  108. <ShortcutTooltip shortcutId={SHORTCUT_IDS.INSPECTOR_JOIN_CHANNEL} side="bottom">
  109. {channelPopoverTrigger}
  110. </ShortcutTooltip>
  111. ) : (
  112. channelPopoverTrigger
  113. )}
  114. <PopoverContent className="p-0 w-[320px]" align="start">
  115. <div className="p-4 flex flex-col text-sm">
  116. {config.channelName.length === 0 ? (
  117. <>
  118. <Form {...form}>
  119. <form
  120. id="realtime-channel"
  121. onSubmit={form.handleSubmit(() => onSubmit())}
  122. className="flex flex-col gap-y-4"
  123. >
  124. <FormField
  125. name="channel"
  126. control={form.control}
  127. render={({ field }) => (
  128. <FormItem className="flex flex-col gap-y-2">
  129. <div className="flex flex-col gap-y-1">
  130. <label className="text-foreground text-xs">Name of channel</label>
  131. <div className="flex flex-row">
  132. <FormControl>
  133. <Input
  134. {...field}
  135. autoComplete="off"
  136. className="rounded-r-none text-xs px-2.5 py-1 h-auto"
  137. placeholder="Enter a channel name"
  138. />
  139. </FormControl>
  140. <Button
  141. type="primary"
  142. className="rounded-l-none"
  143. disabled={form.getValues().channel.length === 0}
  144. onClick={() => onSubmit()}
  145. >
  146. Listen to channel
  147. </Button>
  148. </div>
  149. </div>
  150. <FormDescription className="text-xs text-foreground-lighter">
  151. The channel you initialize with the Briven Realtime client. Learn more
  152. in{' '}
  153. <a
  154. target="_blank"
  155. rel="noreferrer"
  156. className="underline hover:text-foreground transition"
  157. href={`${DOCS_URL}/guides/realtime/concepts#channels`}
  158. >
  159. our docs
  160. </a>
  161. </FormDescription>
  162. </FormItem>
  163. )}
  164. />
  165. <FormField
  166. key="isPrivate"
  167. control={form.control}
  168. name="isPrivate"
  169. render={({ field }) => (
  170. <FormItem className="">
  171. <div className="flex flex-row items-center gap-x-2">
  172. <FormControl>
  173. <Switch
  174. checked={field.value}
  175. onCheckedChange={field.onChange}
  176. disabled={field.disabled}
  177. />
  178. </FormControl>
  179. <FormLabel className="text-xs">Is channel private?</FormLabel>
  180. </div>
  181. <FormDescription className="text-xs text-foreground-lighter mt-2">
  182. If the channel is marked as private, it will use RLS policies to filter
  183. messages.
  184. </FormDescription>
  185. </FormItem>
  186. )}
  187. />
  188. <DocsButton
  189. abbrev={false}
  190. className="w-min"
  191. href={`${DOCS_URL}/guides/realtime/authorization`}
  192. />
  193. </form>
  194. </Form>
  195. </>
  196. ) : (
  197. <div className="space-y-2">
  198. <div className="flex items-center gap-x-2">
  199. <p className="text-foreground text-xs">
  200. Currently joined{' '}
  201. <span className={config.isChannelPrivate ? 'text-brand' : 'text-warning'}>
  202. {config.isChannelPrivate ? 'private' : 'public'}
  203. </span>{' '}
  204. channel:
  205. </p>
  206. <p className="text-xs border border-scale-600 py-0.5 px-1 rounded-md bg-surface-200">
  207. {config.channelName}
  208. </p>
  209. </div>
  210. <p className="text-xs text-foreground-lighter mt-2">
  211. If you leave this channel, all of the messages populated on this page will disappear
  212. </p>
  213. <Button
  214. type="default"
  215. onClick={() => onChangeConfig({ ...config, channelName: '', enabled: false })}
  216. >
  217. Leave channel
  218. </Button>
  219. </div>
  220. )}
  221. </div>
  222. </PopoverContent>
  223. </Popover>
  224. )
  225. }