mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? We've quite a few instances where some buttons have a dropdown appendage. The focus state for these were broken as well as visually regarding the separator. This first pass fixes the instances we have in studio, I've left potentially adding this to our design system fragment components as another PR. | Before | After | |--------|--------| | <img width="531" height="133" alt="Screenshot 2026-08-13 at 11 29 36" src="https://github.com/user-attachments/assets/70747fd0-11d4-4670-85fa-d76f3564837b" /> | <img width="519" height="130" alt="Screenshot 2026-08-13 at 11 40 15" src="https://github.com/user-attachments/assets/5920bebb-81ce-4962-908b-5f61526ca7ca" /> | | <img width="538" height="146" alt="Screenshot 2026-08-13 at 11 29 48" src="https://github.com/user-attachments/assets/f1c7018f-cd43-47fa-b4c0-045af350f80b" /> | <img width="515" height="148" alt="Screenshot 2026-08-13 at 11 39 56" src="https://github.com/user-attachments/assets/fee09113-433a-4b5d-ac68-3ab3f2565d34" /> | | <img width="538" height="143" alt="Screenshot 2026-08-13 at 11 29 52" src="https://github.com/user-attachments/assets/3063d391-99b2-4599-9cdf-edd0b2cdfdf0" /> | <img width="529" height="159" alt="Screenshot 2026-08-13 at 11 40 05" src="https://github.com/user-attachments/assets/4bdeb9e0-8324-45df-a211-8c7fd0ee11a6" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved keyboard focus visibility across Studio controls, including token management, email settings, replication, log drains, query insights, infrastructure, storage, and assistant actions. * Focused buttons in adjacent or split-button groups now appear above neighboring controls, preventing borders and overlays from obscuring the active selection. * Preserved existing button behavior, layout, and appearance while improving focus-state clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
218 lines
5.7 KiB
TypeScript
218 lines
5.7 KiB
TypeScript
import { AiAssistantSource } from 'common/telemetry-constants'
|
|
import { Chatgpt, Claude } from 'icons'
|
|
import { Check, ChevronDown, Copy } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { ComponentProps, ReactNode, useEffect, useState } from 'react'
|
|
import {
|
|
AiIconAnimation,
|
|
Button,
|
|
cn,
|
|
copyToClipboard,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
type TelemetrySource = AiAssistantSource
|
|
|
|
const EXTERNAL_AI_TOOLS = [
|
|
{
|
|
label: 'Ask ChatGPT',
|
|
url: 'https://chatgpt.com/',
|
|
promptParam: 'q',
|
|
icon: Chatgpt,
|
|
toolId: 'chatgpt' as const,
|
|
},
|
|
{
|
|
label: 'Ask Claude',
|
|
url: 'https://claude.ai/new',
|
|
promptParam: 'q',
|
|
icon: Claude,
|
|
toolId: 'claude' as const,
|
|
},
|
|
]
|
|
|
|
export interface AiAssistantDropdownItem {
|
|
label: string
|
|
icon?: ReactNode
|
|
href?: string
|
|
onClick?: () => void
|
|
}
|
|
|
|
export interface AiAssistantDropdownProps {
|
|
buildPrompt: () => string
|
|
label: string
|
|
iconOnly?: boolean
|
|
onOpenAssistant: () => void
|
|
onCopyPrompt?: () => void
|
|
telemetrySource?: TelemetrySource
|
|
size?: ComponentProps<typeof Button>['size']
|
|
variant?: ComponentProps<typeof Button>['variant']
|
|
disabled?: boolean
|
|
loading?: boolean
|
|
className?: string
|
|
tooltip?: string
|
|
copyLabel?: string
|
|
showExternalAI?: boolean
|
|
additionalDropdownItems?: AiAssistantDropdownItem[]
|
|
}
|
|
|
|
export function AiAssistantDropdown({
|
|
buildPrompt,
|
|
label,
|
|
iconOnly = false,
|
|
onOpenAssistant,
|
|
onCopyPrompt,
|
|
telemetrySource,
|
|
size = 'tiny',
|
|
variant = 'default',
|
|
disabled = false,
|
|
loading = false,
|
|
className,
|
|
tooltip,
|
|
copyLabel = 'Copy prompt',
|
|
showExternalAI = false,
|
|
additionalDropdownItems,
|
|
}: AiAssistantDropdownProps) {
|
|
const track = useTrack()
|
|
const [showCopied, setShowCopied] = useState(false)
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!showCopied) return
|
|
const timer = setTimeout(() => setShowCopied(false), 2000)
|
|
return () => clearTimeout(timer)
|
|
}, [showCopied])
|
|
|
|
const handleCopyPrompt = () => {
|
|
const prompt = buildPrompt()
|
|
copyToClipboard(prompt)
|
|
setShowCopied(true)
|
|
setIsOpen(false)
|
|
onCopyPrompt?.()
|
|
|
|
if (telemetrySource) {
|
|
track('ai_prompt_copied', { source: telemetrySource })
|
|
}
|
|
}
|
|
|
|
const handleOpenExternalAI = (tool: (typeof EXTERNAL_AI_TOOLS)[number]) => {
|
|
const prompt = buildPrompt()
|
|
window.open(
|
|
`${tool.url}?${tool.promptParam}=${encodeURIComponent(prompt)}`,
|
|
'_blank',
|
|
'noreferrer'
|
|
)
|
|
|
|
if (telemetrySource) {
|
|
track('ai_external_tool_clicked', { source: telemetrySource, tool: tool.toolId })
|
|
}
|
|
}
|
|
|
|
const handleOpenAssistant = () => {
|
|
onOpenAssistant()
|
|
|
|
if (telemetrySource) {
|
|
track('ai_assistant_dropdown_button_clicked', { source: telemetrySource })
|
|
}
|
|
}
|
|
|
|
const buttonContent = (
|
|
<div className={cn('flex items-center', iconOnly ? 'gap-0' : 'gap-0')}>
|
|
{/* Main button */}
|
|
<Button
|
|
variant={variant}
|
|
size={size}
|
|
disabled={disabled}
|
|
onClick={handleOpenAssistant}
|
|
icon={<AiIconAnimation size={iconOnly ? 16 : 14} loading={loading} />}
|
|
className={cn(
|
|
'rounded-r-none border-r-0 focus-visible:z-10',
|
|
iconOnly && 'px-1.5',
|
|
className
|
|
)}
|
|
>
|
|
{!iconOnly && label}
|
|
</Button>
|
|
|
|
{/* Dropdown trigger */}
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
aria-label="More actions"
|
|
variant={variant}
|
|
size={size}
|
|
disabled={disabled}
|
|
className={cn('rounded-l-none px-1 focus-visible:z-10', iconOnly && 'px-1')}
|
|
icon={<ChevronDown size={12} />}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-44">
|
|
<DropdownMenuItem onClick={handleCopyPrompt} className="gap-2">
|
|
{showCopied ? <Check size={14} className="text-brand" /> : <Copy size={14} />}
|
|
{showCopied ? 'Copied!' : copyLabel}
|
|
</DropdownMenuItem>
|
|
|
|
{showExternalAI && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
{EXTERNAL_AI_TOOLS.map((tool) => (
|
|
<DropdownMenuItem
|
|
key={tool.url}
|
|
className="gap-2"
|
|
onClick={() => handleOpenExternalAI(tool)}
|
|
>
|
|
<tool.icon size={14} />
|
|
{tool.label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{additionalDropdownItems && additionalDropdownItems.length > 0 && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
{additionalDropdownItems.map((item, i) => (
|
|
<DropdownMenuItem key={i} onClick={item.onClick} className="gap-2">
|
|
{item.href ? (
|
|
<Link href={item.href} target="_blank" rel="noreferrer">
|
|
{item.icon}
|
|
{item.label}
|
|
</Link>
|
|
) : (
|
|
<>
|
|
{item.icon}
|
|
{item.label}
|
|
</>
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)
|
|
|
|
// Wrap in tooltip for icon-only mode
|
|
if (iconOnly && tooltip) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="inline-flex">{buttonContent}</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">{tooltip}</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return buttonContent
|
|
}
|