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