mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## Context Building on top of "Database Connections" - this adds a top summary section, again from `pg_stat_activity` <img width="948" height="324" alt="image" src="https://github.com/user-attachments/assets/f4968193-0a5f-4754-a630-40685b747999" /> Each block comes with a tooltip in hopes to educate the significance of each metric - Connections: Spread of connections per database role <img width="313" height="164" alt="image" src="https://github.com/user-attachments/assets/8ceeab5d-b960-4be3-9a5b-8600bd5cf303" /> - Active queries: Rough representative of activity <img width="350" height="196" alt="image" src="https://github.com/user-attachments/assets/f9705ff1-a869-409a-86b6-50170a169674" /> - Idle in transaction: Important to identify as this indicates locks (Suggests root cause) <img width="350" height="196" alt="image" src="https://github.com/user-attachments/assets/f9705ff1-a869-409a-86b6-50170a169674" /> - Blocked queries: Also important to identify stuck queries <img width="335" height="183" alt="image" src="https://github.com/user-attachments/assets/57255fb8-24f6-4ddd-aa54-850a77173b5c" /> - Longest running query: Might be useful to identify unusually long queries - Will be `text-warning` if exceeds 30 seconds for active queries, `text-destructive` if exceeds 10 seconds for queries idle in transaction <img width="342" height="119" alt="image" src="https://github.com/user-attachments/assets/f6783b43-058a-4a32-a40c-0bc64f23d2ce" /> "Summarize activity" CTA leverages on the Assistant to give a quick overview - highlights any potential issues for quick reference <img width="1918" height="958" alt="image" src="https://github.com/user-attachments/assets/340121fe-3186-48a5-8023-fbac2a93397a" /> ## Other changes - Hides "View running queries" in SQL Editor if `topForPostgres` feature flag is enabled (since this UI is meant to replace that) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Database Connections observability overview with metric cards (connections, longest-running, active, blocked, idle-in-transaction) and an interactive “Longest running” PID selector. * Added a “Summarize activity” AI assistant dropdown that starts a timestamped, activity-aware summary chat. * **Improvements** * Enhanced live activity refresh (including window-focus updates) and standardized duration warning thresholds for active and idle-in-transaction sessions. * Improved hover details for query previews and allowed richer tooltip content for metric labels. * **Feature Changes** * Gated the “View running queries” bottom panel behind a feature flag. * **Bug Fixes** * Refined running-too-long badge and warning styling for idle-in-transaction cases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
256 lines
9.9 KiB
TypeScript
256 lines
9.9 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { parseAsInteger, useQueryState } from 'nuqs'
|
|
import { cn } from 'ui'
|
|
import {
|
|
MetricCard,
|
|
MetricCardContent,
|
|
MetricCardHeader,
|
|
MetricCardLabel,
|
|
MetricCardValue,
|
|
} from 'ui-patterns/MetricCard'
|
|
|
|
import { WARN_DURATION_ACTIVE_QUERY, WARN_DURATION_IDLE_TXN } from './DatabaseConnections.constants'
|
|
import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils'
|
|
import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query'
|
|
import { useDatabaseActivityQuery, type DatabaseActivity } from '@/data/database/activity-query'
|
|
import { useMaxConnectionsQuery } from '@/data/database/max-connections-query'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
|
|
const LONG_RUNNING_STATES: (DatabaseActivity['state'] | undefined)[] = [
|
|
'active',
|
|
'idle in transaction',
|
|
'idle in transaction (aborted)',
|
|
]
|
|
|
|
interface OverviewProps {
|
|
live?: boolean
|
|
}
|
|
|
|
/**
|
|
* [Joshen] Couple of nuances worth calling out to provide better signals for the user
|
|
* - Idle in transaction:
|
|
* - Only considers queries in that state, but running for longer than 10 seconds
|
|
* - Could otherwise be a query in mid-flight
|
|
* - Longest running:
|
|
* - Only considers queries that are active or idle in transaction
|
|
*/
|
|
export const Overview = ({ live }: OverviewProps) => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [, setSelectedPid] = useQueryState('pid', parseAsInteger)
|
|
|
|
const { data, isPending: isLoadingActivity } = useDatabaseActivityQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
},
|
|
{ refetchOnWindowFocus: live, refetchInterval: live ? 3000 : false }
|
|
)
|
|
const activeQueries = (data ?? []).filter((x) => x.state === 'active')
|
|
const blockedQueries = (data ?? []).filter((x) => x.blocked_by.length > 0)
|
|
const idleInTransactionQueries = (data ?? []).filter((x) => {
|
|
const isIdleInTransaction =
|
|
x.state === 'idle in transaction' || x.state === 'idle in transaction (aborted)'
|
|
if (!isIdleInTransaction || !x.transaction_start) return false
|
|
return dayjs().utc().diff(dayjs(x.transaction_start).utc(), 'second') > WARN_DURATION_IDLE_TXN
|
|
})
|
|
|
|
const longestRunningQuery = (data ?? [])
|
|
.filter((x) => LONG_RUNNING_STATES.includes(x.state))
|
|
.reduce<{ activity: DatabaseActivity; duration: number } | null>((longest, activity) => {
|
|
const start = activity.state === 'active' ? activity.query_start : activity.transaction_start
|
|
if (!start) return longest
|
|
const duration = Math.max(dayjs().utc().diff(dayjs(start).utc(), 'second'), 0)
|
|
return longest === null || duration > longest.duration ? { activity, duration } : longest
|
|
}, null)
|
|
const warnLongestRunningQuery =
|
|
(longestRunningQuery?.activity.state === 'active' &&
|
|
longestRunningQuery.duration >= WARN_DURATION_ACTIVE_QUERY) ||
|
|
((longestRunningQuery?.activity.state === 'idle in transaction' ||
|
|
longestRunningQuery?.activity.state === 'idle in transaction (aborted)') &&
|
|
longestRunningQuery.duration >= WARN_DURATION_IDLE_TXN)
|
|
|
|
const { data: roles, isPending: isLoadingRoles } = useDatabaseRolesQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
},
|
|
{ refetchOnWindowFocus: live, refetchInterval: live ? 3000 : false }
|
|
)
|
|
const rolesWithActiveConnections = (roles ?? []).filter((role) => role.activeConnections)
|
|
const totalActiveConnections = (roles ?? [])
|
|
.map((role) => role.activeConnections)
|
|
.reduce((a, b) => a + b, 0)
|
|
|
|
const { data: maxConnectionLimit, isPending: isLoadingMaxConnections } = useMaxConnectionsQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
},
|
|
{
|
|
select: (data) => data.maxConnections,
|
|
refetchInterval: live ? 3000 : false,
|
|
}
|
|
)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-y-4">
|
|
<div className="flex gap-x-4">
|
|
<h2>Overview</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<MetricCard isLoading={isLoadingRoles || isLoadingMaxConnections}>
|
|
<MetricCardHeader>
|
|
<MetricCardLabel
|
|
tooltip={
|
|
<div>
|
|
<p className="text-foreground-light pr-2">Connections by roles:</p>
|
|
{rolesWithActiveConnections.map((role) => (
|
|
<div key={role.id} className="flex items-center">
|
|
<p className="min-w-32">{role.name}:</p> {role.activeConnections}
|
|
</div>
|
|
))}
|
|
</div>
|
|
}
|
|
>
|
|
Connections
|
|
</MetricCardLabel>
|
|
</MetricCardHeader>
|
|
<MetricCardContent>
|
|
<MetricCardValue className="space-x-1">
|
|
<span>{totalActiveConnections}</span>
|
|
<span className="text-sm text-foreground-light">/</span>
|
|
<span className="text-sm text-foreground-light">{maxConnectionLimit}</span>
|
|
</MetricCardValue>
|
|
</MetricCardContent>
|
|
</MetricCard>
|
|
|
|
<MetricCard
|
|
isLoading={isLoadingActivity}
|
|
className={cn(warnLongestRunningQuery && 'bg-warning-200 border-warning-400')}
|
|
>
|
|
<MetricCardHeader>
|
|
<MetricCardLabel
|
|
className={cn(warnLongestRunningQuery && 'text-foreground')}
|
|
tooltip="Only considers active or idle-in-transaction queries"
|
|
>
|
|
Longest running
|
|
</MetricCardLabel>
|
|
</MetricCardHeader>
|
|
<MetricCardContent>
|
|
<MetricCardValue
|
|
className={cn(
|
|
'space-x-2',
|
|
longestRunningQuery === null
|
|
? 'text-foreground-lighter'
|
|
: warnLongestRunningQuery
|
|
? 'text-warning'
|
|
: 'text-foreground'
|
|
)}
|
|
>
|
|
{longestRunningQuery === null ? (
|
|
'-'
|
|
) : (
|
|
<>
|
|
<span>{formatDuration(longestRunningQuery.duration * 1000, 0)}</span>
|
|
<span className="text-foreground-lighter text-sm">·</span>
|
|
<span
|
|
role="button"
|
|
tabIndex={0}
|
|
className="text-foreground-lighter text-sm hover:text-foreground transition cursor-pointer"
|
|
onClick={() => setSelectedPid(longestRunningQuery.activity.pid)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
setSelectedPid(longestRunningQuery.activity.pid)
|
|
}
|
|
}}
|
|
>
|
|
PID: {longestRunningQuery.activity.pid}
|
|
</span>
|
|
</>
|
|
)}
|
|
</MetricCardValue>
|
|
</MetricCardContent>
|
|
</MetricCard>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
|
|
<MetricCard isLoading={isLoadingActivity}>
|
|
<MetricCardHeader>
|
|
<MetricCardLabel tooltip="Queries currently executing on the database.">
|
|
Active queries
|
|
</MetricCardLabel>
|
|
</MetricCardHeader>
|
|
<MetricCardContent>
|
|
<MetricCardValue>{activeQueries?.length}</MetricCardValue>
|
|
</MetricCardContent>
|
|
</MetricCard>
|
|
|
|
<MetricCard
|
|
isLoading={isLoadingActivity}
|
|
className={cn(blockedQueries.length && 'bg-destructive-200 border-destructive-400')}
|
|
>
|
|
<MetricCardHeader>
|
|
<MetricCardLabel
|
|
className={cn(blockedQueries.length && 'text-foreground')}
|
|
tooltip={
|
|
<>
|
|
<p>
|
|
Queries waiting on a lock held by another session - stalls everything queued
|
|
behind it.
|
|
</p>
|
|
<p className="mt-2">
|
|
Typically caused by an uncommitted transaction, a long-running migration, or a
|
|
stuck idle-in-transaction session.
|
|
</p>
|
|
</>
|
|
}
|
|
>
|
|
Blocked queries
|
|
</MetricCardLabel>
|
|
</MetricCardHeader>
|
|
<MetricCardContent>
|
|
<MetricCardValue className={cn(blockedQueries.length && 'text-destructive')}>
|
|
{blockedQueries.length}
|
|
</MetricCardValue>
|
|
</MetricCardContent>
|
|
</MetricCard>
|
|
|
|
<MetricCard
|
|
isLoading={isLoadingActivity}
|
|
className={cn(idleInTransactionQueries.length && 'bg-warning-200 border-warning-400')}
|
|
>
|
|
<MetricCardHeader>
|
|
<MetricCardLabel
|
|
className={cn(idleInTransactionQueries.length && 'text-foreground')}
|
|
tooltip={
|
|
<>
|
|
<p>
|
|
Transactions left open without running a query, which can hold locks and block
|
|
table cleanup for as long as it stays open
|
|
</p>
|
|
<p className="mt-2">
|
|
Typically indicates an app issue, such as a forgotten COMMIT or ROLLBACK.
|
|
</p>
|
|
</>
|
|
}
|
|
>
|
|
Idle in transaction
|
|
</MetricCardLabel>
|
|
</MetricCardHeader>
|
|
<MetricCardContent>
|
|
<MetricCardValue
|
|
className={cn(idleInTransactionQueries.length > 0 && 'text-warning')}
|
|
>
|
|
{idleInTransactionQueries.length}
|
|
</MetricCardValue>
|
|
</MetricCardContent>
|
|
</MetricCard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|