MessagesTable.tsx256 lines · main
1// @ts-nocheck
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { isEqual } from 'lodash'
5import { ExternalLink, Loader2, Megaphone } from 'lucide-react'
6import Link from 'next/link'
7import { useEffect, useState } from 'react'
8import DataGrid, { Row } from 'react-data-grid'
9import { Button, cn, IconBroadcast, IconDatabaseChanges, IconPresence } from 'ui'
10import { GenericSkeletonLoader } from 'ui-patterns'
11
12import type { LogData } from './Messages.types'
13import MessageSelection from './MessageSelection'
14import NoChannelEmptyState from './NoChannelEmptyState'
15import { ColumnRenderer } from './RealtimeMessageColumnRenderer'
16import { DocsButton } from '@/components/ui/DocsButton'
17import NoPermission from '@/components/ui/NoPermission'
18import ShimmerLine from '@/components/ui/ShimmerLine'
19import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
20import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
21import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
22import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
23import { DOCS_URL } from '@/lib/constants'
24import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
25
26export const isErrorLog = (l: LogData) => {
27 return l.message === 'SYSTEM' && l.metadata?.status === 'error'
28}
29
30const NoResultAlert = ({
31 enabled,
32 hasChannelSet,
33 showSendMessage,
34}: {
35 enabled: boolean
36 hasChannelSet: boolean
37 showSendMessage: () => void
38}) => {
39 const { ref } = useParams()
40
41 const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
42 PermissionAction.READ,
43 'service_api_keys'
44 )
45
46 const broadcastButton = (
47 <Button type="default" onClick={showSendMessage}>
48 Broadcast a message
49 </Button>
50 )
51
52 return (
53 <div className="w-full max-w-md flex items-center flex-col">
54 {isLoadingPermissions ? (
55 <GenericSkeletonLoader className="w-80" />
56 ) : !canReadAPIKeys ? (
57 <NoPermission isFullPage resourceText="use the realtime inspector" />
58 ) : !hasChannelSet ? (
59 <NoChannelEmptyState />
60 ) : (
61 <>
62 {enabled && <p className="text-foreground">No Realtime messages found</p>}
63 <p className="text-foreground-lighter">Realtime message logs will be shown here</p>
64
65 <div className="mt-4 border bg-surface-100 border-border rounded-md justify-start items-center flex flex-col w-full">
66 <div className="w-full px-5 py-4 items-center gap-4 inline-flex border-b">
67 <IconBroadcast
68 size="xlarge"
69 className="text-background bg-foreground rounded-sm w-6"
70 />
71 <div className="grow flex-col flex">
72 <p className="text-foreground">Create a Broadcast message</p>
73 <p className="text-foreground-lighter text-xs">Send a message in the channel</p>
74 </div>
75 {enabled ? (
76 <ShortcutTooltip shortcutId={SHORTCUT_IDS.INSPECTOR_BROADCAST} side="bottom">
77 {broadcastButton}
78 </ShortcutTooltip>
79 ) : (
80 broadcastButton
81 )}
82 </div>
83 <div className="w-full px-5 py-4 items-center gap-4 inline-flex border-b">
84 <IconPresence
85 size="xlarge"
86 className="text-background bg-foreground rounded-sm w-6"
87 />
88 <div className="grow flex-col flex">
89 <p className="text-foreground">Join from another browser tab</p>
90 <p className="text-foreground-lighter text-xs">
91 Send messages between multiple clients
92 </p>
93 </div>
94 <Link href={`/project/${ref}/realtime/inspector`} target="_blank" rel="noreferrer">
95 <Button type="default" iconRight={<ExternalLink />}>
96 Open inspector
97 </Button>
98 </Link>
99 </div>
100
101 <div className="w-full px-5 py-4 items-center gap-4 inline-flex border-b">
102 <IconDatabaseChanges
103 size="xlarge"
104 className="text-background bg-foreground rounded-sm w-6"
105 />
106 <div className="grow flex-col flex">
107 <p className="text-foreground">Listen to a table for changes</p>
108 <p className="text-foreground-lighter text-xs">Tables must have realtime enabled</p>
109 </div>
110 <Link href={`/project/${ref}/database/publications`} target="_blank" rel="noreferrer">
111 <Button type="default" iconRight={<ExternalLink />}>
112 Publications settings
113 </Button>
114 </Link>
115 </div>
116 <div className="w-full px-5 py-4 items-center gap-4 inline-flex rounded-b-md bg-studio">
117 <div className="grow flex-col flex">
118 <p className="text-foreground">Not sure what to do?</p>
119 <p className="text-foreground-lighter text-xs">Browse our documentation</p>
120 </div>
121 <DocsButton href={`${DOCS_URL}/guides/realtime`} />
122 </div>
123 </div>
124 </>
125 )}
126 </div>
127 )
128}
129
130interface MessagesTableProps {
131 enabled: boolean
132 data?: LogData[]
133 hasChannelSet: boolean
134 showSendMessage: () => void
135}
136
137const MessagesTable = ({
138 enabled,
139 hasChannelSet,
140 data = [],
141 showSendMessage,
142}: MessagesTableProps) => {
143 const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
144 const stringData = JSON.stringify(data)
145
146 const { ref } = useParams()
147 const { data: org } = useSelectedOrganizationQuery()
148 const { mutate: sendEvent } = useSendEventMutation()
149
150 useEffect(() => {
151 if (!data) return
152 const found = data.find((datum) => datum.id === focusedLog?.id)
153 if (!found) {
154 setFocusedLog(null)
155 }
156 }, [stringData])
157
158 if (!data) return null
159
160 return (
161 <>
162 <section className="flex w-full flex-col md:max-h-[calc(100vh-var(--header-height)-3rem)]">
163 <ShimmerLine active={enabled} />
164 <div
165 className={cn(
166 'grid grid-rows-2 grid-cols-1 lg:flex h-full lg:flex-row',
167 enabled ? 'border-brand-400' : null
168 )}
169 >
170 <div className="flex grow flex-col lg:w-1/2">
171 {enabled && (
172 <div className="w-full h-9 px-4 bg-surface-100 items-center inline-flex justify-between text-foreground-light">
173 <div className="inline-flex gap-2.5 text-xs">
174 <Loader2 size="16" className="animate-spin" />
175 <div>Listening</div>
176 <div>•</div>
177 <div>
178 {data.length > 0
179 ? data.length >= 100
180 ? `Found a large number of messages, showing only the latest 100...`
181 : `Found ${data.length} messages...`
182 : `No message found yet...`}
183 </div>
184 </div>
185 <ShortcutTooltip shortcutId={SHORTCUT_IDS.INSPECTOR_BROADCAST} side="bottom">
186 <Button
187 type="default"
188 onClick={showSendMessage}
189 icon={<Megaphone strokeWidth={1.5} />}
190 >
191 <span>Broadcast a message</span>
192 </Button>
193 </ShortcutTooltip>
194 </div>
195 )}
196
197 <DataGrid
198 className="data-grid--simple-logs h-full border-b-0"
199 rowHeight={40}
200 headerRowHeight={0}
201 columns={ColumnRenderer}
202 rowClass={(row) => {
203 return cn([
204 'font-mono tracking-tight',
205 isEqual(row, focusedLog)
206 ? 'bg-scale-800 rdg-row--focused'
207 : 'bg-200 hover:bg-scale-300 cursor-pointer',
208 isErrorLog(row) && 'bg-warning-300!',
209 ])
210 }}
211 rows={data}
212 rowKeyGetter={(row) => row.id}
213 renderers={{
214 renderRow(idx, props) {
215 const { row } = props
216 return (
217 <Row
218 key={idx}
219 {...props}
220 isRowSelected={false}
221 selectedCellIdx={undefined}
222 onClick={() => {
223 sendEvent({
224 action: 'realtime_inspector_message_clicked',
225 groups: {
226 project: ref ?? 'Unknown',
227 organization: org?.slug ?? 'Unknown',
228 },
229 })
230 setFocusedLog(row)
231 }}
232 />
233 )
234 },
235 noRowsFallback: (
236 <div className="mx-auto flex h-full w-full items-center justify-center space-y-12 py-4 transition-all delay-200 duration-500">
237 <NoResultAlert
238 enabled={enabled}
239 hasChannelSet={hasChannelSet}
240 showSendMessage={showSendMessage}
241 />
242 </div>
243 ),
244 }}
245 />
246 </div>
247
248 <div className="flex lg:w-1/2 flex-col">
249 <MessageSelection onClose={() => setFocusedLog(null)} log={focusedLog} />
250 </div>
251 </div>
252 </section>
253 </>
254 )
255}
256export default MessagesTable