Files
supabase/apps/studio/components/interfaces/Realtime/Inspector/MessageSelection.tsx
Ali WaseemandDanny White 3d9fb2f98d feat(realtime): shortcuts for realtime (#46001)
## What kind of change does this PR introduce?

Feature — adds keyboard shortcuts for the Realtime section in Studio.

## What is the current behavior?

No keyboard shortcuts exist for Realtime navigation or inspector
actions.

## What is the new behavior?

### Navigation chords (layout-scoped, active on any Realtime page)

| Shortcut | Action |
|---|---|
| `R I` | Go to Inspector |
| `R P` | Go to Policies |
| `R S` | Go to Settings |

### Inspector actions (active on the Inspector page)

| Shortcut | Action | Gating |
|---|---|---|
| `Shift+J` | Join a channel | Only when no channel is joined |
| `Shift+L` | Start/Stop listening | Only when a channel is joined |
| `Shift+F` | Open filter popover | Only when a channel is joined |
| `Shift+B` | Broadcast a message | Only when listening |
| `Mod+Shift+C` | Copy selected message | Only when a message is
selected |


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Realtime Inspector: new keyboard shortcuts (join channel, toggle
listening, toggle filters, broadcast) and a copy-message shortcut with
toast feedback.
* Channel & Filter popovers support controlled/uncontrolled open state;
header now wires popover state through props.
* Shortcut tooltips added to copy and broadcast actions; realtime page
navigation shortcuts and menu shortcuts added.
* **Tests**
* Shortcut reference sheet tests updated to include realtime navigation
and inspector groups.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46001?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
2026-05-19 11:12:54 -06:00

128 lines
4.5 KiB
TypeScript

import { useParams } from 'common'
import { X } from 'lucide-react'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { Button, cn, copyToClipboard } from 'ui'
import type { LogData } from './Messages.types'
import { SelectedRealtimeMessagePanel } from './SelectedRealtimeMessagePanel'
import CopyButton from '@/components/ui/CopyButton'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
export interface MessageSelectionProps {
log: LogData | null
onClose: () => void
}
const MessageSelection = ({ log, onClose }: MessageSelectionProps) => {
const selectionText = useMemo(() => {
return JSON.stringify(log, null, 2)
}, [log])
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const { mutate: sendEvent } = useSendEventMutation()
const handleCopy = () => {
if (!log) return
copyToClipboard(selectionText)
toast.success('Message copied to clipboard')
}
useShortcut(SHORTCUT_IDS.INSPECTOR_COPY_MESSAGE, handleCopy, {
enabled: !!log,
ignoreInputs: true,
registerInCommandMenu: true,
})
const copyButton = (
<CopyButton
text={selectionText}
type="default"
title="Copy log to clipboard"
onClick={() => {
sendEvent({
action: 'realtime_inspector_copy_message_clicked',
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
/>
)
return (
<div className={cn('relative flex h-full grow flex-col border-l overflow-y-scroll bg-200')}>
<div
className={cn(
'absolute flex h-full w-full flex-col items-center justify-center gap-2 bg-200 text-center opacity-0 transition-all',
log ? 'z-0 opacity-0' : 'z-10 opacity-100'
)}
>
<div
className={cn(
'flex w-full max-w-sm scale-95 flex-col items-center justify-center gap-6 text-center opacity-0 transition-all delay-300 duration-500',
log ? 'mt-0 scale-95 opacity-0' : 'mt-8 scale-100 opacity-100'
)}
>
<div className="relative flex h-4 w-32 items-center rounded-sm border border-default px-2">
<div className="h-0.5 w-2/3 rounded-full bg-overlay-hover"></div>
<div className="absolute right-1 -bottom-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
</div>
</div>
<div className="flex flex-col gap-1">
<h3 className="text-sm text-foreground">Select a message</h3>
<p className="text-xs text-foreground-lighter">
Click on a message on the left to view details.
</p>
</div>
</div>
</div>
<div className="relative h-px grow">
<div className="pt-4 flex flex-col gap-4">
<div className="px-4 flex flex-row justify-between items-center">
<div className="transition">
{log ? (
<ShortcutTooltip shortcutId={SHORTCUT_IDS.INSPECTOR_COPY_MESSAGE} side="bottom">
{copyButton}
</ShortcutTooltip>
) : (
copyButton
)}
</div>
<Button
type="text"
className="cursor-pointer transition hover:text-scale-1200 h-8 w-8 px-0 py-0 flex items-center justify-center"
onClick={onClose}
>
<X size={14} strokeWidth={2} className="text-scale-900" />
</Button>
</div>
<div className="h-px w-full bg-scale-600 rounded-sm" />
</div>
<div className="flex flex-col space-y-6 py-4">
{log && <SelectedRealtimeMessagePanel log={log} />}
</div>
</div>
</div>
)
}
export default MessageSelection