import { ChevronRight, CircleX, Minus, MoreVertical, StopCircle } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { Fragment, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, cn, copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger, HoverCard, HoverCardContent, HoverCardTrigger, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { CodeBlock } from 'ui-patterns/CodeBlock' import { QUERY_STATE_TOOLTIP, WARN_DURATION_ACTIVE_QUERY, WARN_DURATION_IDLE_TXN, } from './DatabaseConnections.constants' import { getBadgeVariant, getBlockChain, getBlockingChain, getDuration, } from './DatabaseConnections.utils' import { useSelectActivityPid } from './useSelectActivityPid' import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' import { InlineLinkClassName } from '@/components/ui/InlineLink' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { useDatabaseActivityQuery, type DatabaseActivity } from '@/data/database/activity-query' import { useQueryCancelMutation } from '@/data/sql/cancel-query-mutation' import { useSessionTerminateMutation } from '@/data/sql/terminate-session-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { formatSql } from '@/lib/formatSql' import { useTrack } from '@/lib/telemetry/track' export const GroupedActivityRow = ({ activity }: { activity: DatabaseActivity }) => { const { data: project } = useSelectedProjectQuery() const [view] = useQueryState('view', parseAsString.withDefault('')) const [expanded, setExpanded] = useState(false) const { data } = useDatabaseActivityQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const queriesBlockedBy = getBlockingChain(activity.pid, data ?? []) .map((pid) => data?.find((x) => x.pid === pid)) .filter((x) => x !== undefined) return ( <> 0 && view === 'blockers' ? () => setExpanded((prev) => !prev) : undefined } /> {expanded && view === 'blockers' && queriesBlockedBy.map((x, index) => ( ))} ) } export const ActivityRow = ({ activity, expanded, nested, isLast, onExpand, }: { activity: DatabaseActivity expanded?: boolean nested?: boolean isLast?: boolean onExpand?: () => void }) => { const track = useTrack() const { data: project } = useSelectedProjectQuery() const [showTerminateConfirmDialog, setShowTerminateConfirmDialog] = useState(false) const { selectedPid, selectPid } = useSelectActivityPid() const rowRef = useRef(null) const { data } = useDatabaseActivityQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: roles } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const superuserRoles = roles?.filter((role) => role.isSuperuser).map((role) => role.name) const { mutateAsync: cancelQuery } = useQueryCancelMutation({ onError: () => {}, // [Joshen] Error handled at call site }) const { mutateAsync: terminateSession } = useSessionTerminateMutation({ onSuccess: () => { toast.success(`Successfully terminated session (ID: ${activity.pid})`) }, }) const durationSeconds = getDuration(activity) const badgeVariant = getBadgeVariant(activity) /** * Queries in "active state": 30s threshold is long enough (most CRUD queries should be quick) * Queries in "idle in transaction" state: This actively holds locks and blocks autovacuum while contributing nothing, so important to surface early at 10s threshold */ const queryRunningLongWarning = !!durationSeconds && ((activity.state === 'active' && durationSeconds >= WARN_DURATION_ACTIVE_QUERY) || ((activity.state === 'idle in transaction' || activity.state === 'idle in transaction (aborted)') && durationSeconds >= WARN_DURATION_IDLE_TXN)) const onCancelQuery = async (origin: 'dropdown_menu' | 'terminate_dialog') => { const isBlocking = (data ?? []).some((x) => x.blocked_by.includes(activity.pid)) track('query_cancel_button_clicked', { activityState: activity.state, isBlocking, origin, }) const toastId = toast.loading(`Cancelling query (ID: ${activity.pid})`) try { await cancelQuery({ pid: activity.pid, backendStart: activity.backend_start, projectRef: project?.ref, connectionString: project?.connectionString, }) toast.success(`Successfully cancelled query (ID: ${activity.pid})`, { id: toastId }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' toast.error(`Failed to cancel query: ${errorMessage}`, { id: toastId }) } } const onSelectTerminate = () => { const isBlocking = (data ?? []).some((x) => x.blocked_by.includes(activity.pid)) track('session_terminate_button_clicked', { activityState: activity.state, isBlocking, }) setShowTerminateConfirmDialog(true) } const onConfirmTerminate = async () => { const isBlocking = (data ?? []).some((x) => x.blocked_by.includes(activity.pid)) track('session_terminate_submitted', { activityState: activity.state, isBlocking }) try { await terminateSession({ pid: activity.pid, backendStart: activity.backend_start, projectRef: project?.ref, connectionString: project?.connectionString, }) } catch (error) {} } useEffect(() => { if (selectedPid === activity.pid) { rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }, [selectedPid, activity.pid]) return ( <> td]:py-3', nested && 'bg-alternative')} > {selectedPid === activity.pid && (
)} {/* Absolute (not inline in the flex row) so top-0/bottom-0 ignore the cell's padding and touch the adjacent row */} {nested && (isLast ? (
) : ( <>
))} {/* Starts right below the expand button (row's own py-3 top padding + button height), reaches bottom-0 to touch the first nested row's border */} {!!onExpand && expanded && (
)}
{nested &&
} {!!onExpand && (

{!!activity.query ? activity.query : 'No query'}

{activity.query && ( code]:text-xs max-h-64', '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11' )} wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-0')} language="pgsql" value={formatSql(activity.query)} /> )}
{ toast.success('Copied PID') copyToClipboard(activity.pid.toString()) }} > PID: {activity.pid} Click to copy · {activity.role_name} {activity.application_name && ( <> · {activity.application_name} )}

{durationSeconds !== null ? ( formatDuration(durationSeconds * 1000, 0) ) : ( )}

{activity.blocked_by.length > 0 ? ( activity.blocked_by.map((pid, index) => { const blockChain = getBlockChain(pid, data ?? []) return ( {index > 0 && ', '} selectPid(pid)} > {pid}

Blocked via {blockChain.length} hop{blockChain.length > 1 ? 's' : ''}

{blockChain.map((chainPid, chainIndex) => { const chainProcess = data?.find((x) => x.pid === chainPid) const isLastProcess = chainIndex === blockChain.length - 1 return (
{chainIndex > 0 && (
)}
selectPid(chainPid)} > PID: {chainPid} {isLastProcess ? ( Holding lock ) : ( Waiting )}

{chainProcess?.query}

{isLastProcess && (
{chainProcess?.role_name} · {chainProcess?.application_name && ( {chainProcess.application_name} )}
)}
) })}
) }) ) : ( )}