mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## 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 --> [](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>
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { IS_PLATFORM, useParams } from 'common'
|
|
import { PlayCircle, StopCircle } from 'lucide-react'
|
|
import { Dispatch, SetStateAction, useCallback } from 'react'
|
|
import { Button } from 'ui'
|
|
|
|
import { ChooseChannelPopover } from './ChooseChannelPopover'
|
|
import { RealtimeFilterPopover } from './RealtimeFilterPopover'
|
|
import { RealtimeTokensPopover } from './RealtimeTokensPopover'
|
|
import { RealtimeConfig } from './useRealtimeMessages'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
|
|
import { getTemporaryAPIKey } from '@/data/api-keys/temp-api-keys-query'
|
|
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
interface HeaderProps {
|
|
config: RealtimeConfig
|
|
onChangeConfig: Dispatch<SetStateAction<RealtimeConfig>>
|
|
channelPopoverOpen: boolean
|
|
onChannelPopoverChange: (open: boolean) => void
|
|
filterPopoverOpen: boolean
|
|
onFilterPopoverChange: (open: boolean) => void
|
|
}
|
|
|
|
export const Header = ({
|
|
config,
|
|
onChangeConfig,
|
|
channelPopoverOpen,
|
|
onChannelPopoverChange,
|
|
filterPopoverOpen,
|
|
onFilterPopoverChange,
|
|
}: HeaderProps) => {
|
|
const { mutate: sendEvent } = useSendEventMutation()
|
|
const { ref } = useParams()
|
|
const { data: org } = useSelectedOrganizationQuery()
|
|
|
|
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
|
|
PermissionAction.READ,
|
|
'service_api_keys'
|
|
)
|
|
|
|
const canToggleListening = canReadAPIKeys && config.channelName.length > 0
|
|
|
|
const handleToggleListening = useCallback(async () => {
|
|
const willStartListening = !config.enabled
|
|
// [Joshen] Refresh if starting to listen + using temp API key, since it has a low refresh rate
|
|
if (willStartListening && (config.token.startsWith('sb_temp') || !IS_PLATFORM)) {
|
|
const data = await getTemporaryAPIKey({ projectRef: config.projectRef, expiry: 3600 })
|
|
const token = data.api_key
|
|
onChangeConfig({ ...config, token, enabled: !config.enabled })
|
|
} else {
|
|
onChangeConfig({ ...config, enabled: !config.enabled })
|
|
}
|
|
|
|
if (willStartListening) {
|
|
sendEvent({
|
|
action: 'realtime_inspector_listen_channel_clicked',
|
|
groups: {
|
|
project: ref ?? 'Unknown',
|
|
organization: org?.slug ?? 'Unknown',
|
|
},
|
|
})
|
|
}
|
|
}, [config, onChangeConfig, sendEvent, ref, org])
|
|
|
|
useShortcut(SHORTCUT_IDS.INSPECTOR_TOGGLE_LISTENING, handleToggleListening, {
|
|
enabled: canToggleListening,
|
|
})
|
|
|
|
const listeningButton = (
|
|
<Button
|
|
size="tiny"
|
|
type={config.enabled ? 'warning' : 'primary'}
|
|
className="rounded-l-none border-l-0"
|
|
disabled={!canToggleListening}
|
|
icon={config.enabled ? <StopCircle size="16" /> : <PlayCircle size="16" />}
|
|
onClick={handleToggleListening}
|
|
>
|
|
{config.enabled ? `Stop listening` : `Start listening`}
|
|
</Button>
|
|
)
|
|
|
|
return (
|
|
<div className="flex flex-row min-h-14 md:min-h-(--header-height) gap-2.5 items-center px-4 border-b ">
|
|
<div className="flex flex-row">
|
|
<ChooseChannelPopover
|
|
config={config}
|
|
onChangeConfig={onChangeConfig}
|
|
open={channelPopoverOpen}
|
|
onOpenChange={onChannelPopoverChange}
|
|
/>
|
|
<RealtimeTokensPopover config={config} onChangeConfig={onChangeConfig} />
|
|
{canToggleListening ? (
|
|
<ShortcutTooltip shortcutId={SHORTCUT_IDS.INSPECTOR_TOGGLE_LISTENING} side="bottom">
|
|
{listeningButton}
|
|
</ShortcutTooltip>
|
|
) : (
|
|
<ButtonTooltip
|
|
size="tiny"
|
|
type={config.enabled ? 'warning' : 'primary'}
|
|
className="rounded-l-none border-l-0"
|
|
disabled
|
|
icon={config.enabled ? <StopCircle size="16" /> : <PlayCircle size="16" />}
|
|
onClick={handleToggleListening}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: !canReadAPIKeys
|
|
? 'You need additional permissions to use the realtime inspector'
|
|
: config.channelName.length === 0
|
|
? 'You need to join a channel first'
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
{config.enabled ? `Stop listening` : `Start listening`}
|
|
</ButtonTooltip>
|
|
)}
|
|
</div>
|
|
<RealtimeFilterPopover
|
|
config={config}
|
|
onChangeConfig={onChangeConfig}
|
|
open={filterPopoverOpen}
|
|
onOpenChange={onFilterPopoverChange}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|