Files
supabase/apps/studio/components/ui/SortableSection.tsx
Joshen Lim 4dc973048d Add confirmation modal when running notebook if notebook contains query cells that aren't read only (#49376)
## Context

Adds a confirmation modal when hitting "run notebook" if the notebook
contains any query cells that involve any sort of mutation (insert,
update, alter, etc, etc). Also gives users the option to run the
notebook's read only cells as an alternative.

<img width="432" height="355" alt="image"
src="https://github.com/user-attachments/assets/0413a3ad-5419-4c83-8bf3-976bfa683b9a"
/>


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

* **New Features**
* Added confirmation prompts before running queries that may modify data
or database structure.
* Prompts identify potentially mutating notebook queries and allow
running read-only cells instead.
* Query execution now includes checks for destructive operations and
missing row-level security, with optional automatic setup.
* Notebook runs use the latest saved and unsaved SQL and reliably reset
execution status.

* **Bug Fixes**
* Improved notebook layout behavior so content shrinks correctly within
flexible sections.

* **Tests**
* Expanded coverage for mutation detection, comments, multiple
statements, live SQL, and cell filtering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-24 14:15:56 +08:00

94 lines
2.7 KiB
TypeScript

import { useDndMonitor } from '@dnd-kit/core'
import { useSortable } from '@dnd-kit/sortable'
import { GripVertical } from 'lucide-react'
import type { CSSProperties, PropsWithChildren, ReactNode } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Button, cn, DropdownMenu, DropdownMenuTrigger } from 'ui'
export const SortableSection = ({
id,
children,
actions,
gripClassName,
gripDropdownContent,
}: PropsWithChildren<{
id: string
gripClassName?: string
actions?: ReactNode
gripDropdownContent?: ReactNode
}>) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id,
})
const [menuOpen, setMenuOpen] = useState(false)
const isDraggingRef = useRef(false)
const openTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
useDndMonitor({
onDragStart: (event) => {
if (event.active.id === id) isDraggingRef.current = true
},
onDragEnd: (event) => {
if (event.active.id === id) isDraggingRef.current = false
},
onDragCancel: (event) => {
if (event.active.id === id) isDraggingRef.current = false
},
})
useEffect(() => () => clearTimeout(openTimeoutRef.current), [])
const style: CSSProperties = {
transform: transform
? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`
: undefined,
transition,
}
return (
<div
ref={setNodeRef}
style={style}
className="group relative will-change-transform flex items-start gap-x-4 min-w-0"
>
<div className={cn('flex items-center', gripClassName)}>
{actions}
<DropdownMenu
open={menuOpen}
onOpenChange={(open) => {
clearTimeout(openTimeoutRef.current)
if (!open) {
setMenuOpen(false)
return
}
openTimeoutRef.current = setTimeout(() => {
if (!isDraggingRef.current) setMenuOpen(true)
}, 150)
}}
>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="text"
aria-label="Drag to reorder section"
className={cn(
'w-6 text-foreground-muted hover:text-foreground cursor-grab active:cursor-grabbing',
'rounded-sm focus-ring'
)}
{...attributes}
{...listeners}
tabIndex={0}
icon={<GripVertical />}
/>
</DropdownMenuTrigger>
{gripDropdownContent}
</DropdownMenu>
</div>
<div className={cn('w-full min-w-0', isDragging && 'opacity-70')}>{children}</div>
</div>
)
}