import dayjs from 'dayjs' import { isEqual } from 'lodash' import { Minus, MoreVertical, StopCircle, X } from 'lucide-react' import { parseAsArrayOf, parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs' import { Fragment, useEffect, useState } from 'react' import { toast } from 'sonner' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, cn, copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, HoverCard, HoverCardContent, HoverCardTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ReportsSelectFilter } from '../../Reports/v2/ReportsSelectFilter' import { QUERY_STATE_TOOLTIP, WARN_DURATION_ACTIVE_QUERY, WARN_DURATION_IDLE_TXN, } from './DatabaseConnections.constants' import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' 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 { useQueryAbortMutation } from '@/data/sql/abort-query-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { formatSql } from '@/lib/formatSql' const getDuration = (activity: DatabaseActivity) => { const { state } = activity if (state === 'active' && activity.query_start) { return dayjs().utc().diff(dayjs(activity.query_start).utc(), 'second') } if (state === 'idle' && activity.state_change) { return dayjs().utc().diff(dayjs(activity.state_change).utc(), 'second') } if ( (state === 'idle in transaction' || state === 'idle in transaction (aborted)') && activity.transaction_start ) { return dayjs().utc().diff(dayjs(activity.transaction_start).utc(), 'second') } return null } const getBadgeVariant = (activity: DatabaseActivity) => { const { state } = activity if (state === 'active') return 'success' if (state === 'idle in transaction' || state === 'idle in transaction (aborted)') return 'warning' return 'default' } const DEFAULT_ROLES_FILTER = ['anon', 'authenticated', 'postgres'] interface ActivityProps { live?: boolean } export const Activity = ({ live }: ActivityProps) => { const { data: project } = useSelectedProjectQuery() const [selectedPid] = useQueryState('pid', parseAsInteger) const [ { states: statesFilter, applications: applicationsFilter, roles: rolesFilter }, setQueryStates, ] = useQueryStates({ states: parseAsArrayOf(parseAsString, ',').withDefault([]), applications: parseAsArrayOf(parseAsString, ',').withDefault([]), roles: parseAsArrayOf(parseAsString, ',').withDefault(DEFAULT_ROLES_FILTER), }) const hasNoFiltersApplied = statesFilter.length === 0 && applicationsFilter.length === 0 && isEqual(rolesFilter, DEFAULT_ROLES_FILTER) const { data, isPending, isSuccess } = useDatabaseActivityQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, { refetchOnWindowFocus: live, refetchInterval: live ? 3000 : false } ) const { data: roles } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const activities = data?.filter((activity) => { const matchesState = !statesFilter || statesFilter.length === 0 || (activity.state !== null && statesFilter.includes(activity.state)) const matchesRole = rolesFilter.length === 0 || rolesFilter.includes(activity.role_name) const matchesApplication = applicationsFilter.length === 0 || applicationsFilter.includes(activity.application_name) return matchesState && matchesRole && matchesApplication }) const stateOptions = [ 'Idle', 'Active', 'Idle in transaction', 'Idle in transaction (aborted)', 'Fastpath function call', 'Disabled', ].map((x) => ({ label: x, value: x.toLowerCase(), quantity: data?.filter( (y) => y.state === x.toLowerCase() && (rolesFilter.length === 0 || rolesFilter.includes(y.role_name)) && (applicationsFilter.length === 0 || applicationsFilter.includes(y.application_name)) ).length, })) const applicationOptions = Array.from(new Set(data?.map((x) => x.application_name) ?? [])) .sort() .map((x) => ({ label: x, value: x, quantity: data?.filter( (y) => y.application_name === x && (rolesFilter.length === 0 || rolesFilter.includes(y.role_name)) && (!statesFilter || statesFilter.length === 0 || (y.state !== null && statesFilter.includes(y.state))) ).length, })) .filter((x) => !!x.value) const priorityRoles = ['anon', 'authenticated', 'postgres'] const roleOptions = (roles ?? []) .map((x) => ({ label: x.name, value: x.name, quantity: data?.filter( (y) => y.role_name === x.name && (!statesFilter || statesFilter.length === 0 || (y.state !== null && statesFilter.includes(y.state))) && (applicationsFilter.length === 0 || applicationsFilter.includes(y.application_name)) ).length, })) .sort((a, b) => { const aIndex = priorityRoles.indexOf(a.value) const bIndex = priorityRoles.indexOf(b.value) if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex if (aIndex !== -1) return -1 if (bIndex !== -1) return 1 return 0 }) useEffect(() => { if (selectedPid && isSuccess) { document .getElementById(selectedPid.toString()) ?.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }, [selectedPid, isSuccess]) return (
{!!activity.query ? activity.query : 'No query'}
{durationSeconds !== null ? (
formatDuration(durationSeconds * 1000, 0)
) : (
{blockedProcess?.query}
PID: {blockedProcess?.pid} · {blockedProcess?.role_name} {blockedProcess?.application_name && ( <> · {blockedProcess?.application_name} > )}