Files
supabase/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
Joshen Lim 60be899fdb Selecting a PID from the overview card should clear filters if not visible in the UI (#49135)
## Context

For Database Connections - the PIDs on the overview cards are selectable
such that clicking on them should scroll the browser down to where the
row is.

However, if the selected PID isn't rendered due to the applied filters,
clicking on it will seemingly do nothing. Changes here hence opt to
remove all filters then scroll to the selected PID into view, so that
users can always quickly find which PID the overview card is
referencing.

Also chucked in some refactors to centralize the management of filters,
and functionality of selecting a PID into their own hooks

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

## Summary by CodeRabbit

* **New Features**
* Added shared filtering for database activity by state, role,
application, search text, and view.
* Activity filters are now preserved in the URL for easier navigation
and sharing.
* Selecting activity metrics or process IDs now automatically reveals
the relevant activity row.
* Blocker view highlights root activities that are blocking other
queries.

* **Bug Fixes**
* Improved selection behavior when the chosen activity is hidden by
active filters.

* **Tests**
* Added coverage for individual, combined, case-insensitive, and
blocker-specific filtering scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-17 17:45:02 +08:00

349 lines
13 KiB
TypeScript

import { cn } from 'ui'
import {
MetricCard,
MetricCardContent,
MetricCardHeader,
MetricCardLabel,
MetricCardValue,
} from 'ui-patterns/MetricCard'
import { getConnectionMetrics } from './DatabaseConnections.utils'
import { useSelectActivityPid } from './useSelectActivityPid'
import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils'
import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query'
import { useDatabaseActivityQuery } from '@/data/database/activity-query'
import { useMaxConnectionsQuery } from '@/data/database/max-connections-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
interface OverviewProps {
live?: boolean
}
export const Overview = ({ live }: OverviewProps) => {
const track = useTrack()
const { data: project } = useSelectedProjectQuery()
const { selectPid } = useSelectActivityPid()
const { data, isPending: isLoadingActivity } = useDatabaseActivityQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{ refetchOnWindowFocus: live, refetchInterval: live ? 3000 : false }
)
const {
activeQueries,
blockedQueries,
warnBlockedQueries,
longestBlockedQuery,
idleInTransactionQueries,
longestRunningQuery,
warnLongestRunningQuery,
queryBlockingTheMostQueries,
warnTopBlocker,
} = getConnectionMetrics(data ?? [])
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,
}
)
const onSelectLongestBlocked = () => {
if (!longestBlockedQuery) return
track('database_connections_overview_metric_card_clicked', { type: 'longest_blocked' })
selectPid(longestBlockedQuery.activity.pid)
}
const onSelectTopBlocker = () => {
if (!queryBlockingTheMostQueries) return
track('database_connections_overview_metric_card_clicked', { type: 'top_blocker' })
selectPid(queryBlockingTheMostQueries.activity.pid)
}
const onSelectLongestRunning = () => {
if (!longestRunningQuery) return
track('database_connections_overview_metric_card_clicked', { type: 'longest_running' })
selectPid(longestRunningQuery.activity.pid)
}
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 md:grid-cols-3 gap-2">
<MetricCard isLoading={isLoadingRoles || isLoadingMaxConnections}>
<MetricCardHeader>
<MetricCardLabel
tooltip={
<div>
<p className="text-foreground-light">Connections by roles:</p>
<table>
<tbody>
{rolesWithActiveConnections.map((role) => (
<tr key={role.id}>
<th scope="row" className="text-left font-normal">
{role.name}:
</th>
<td className="pl-2">{role.activeConnections}</td>
</tr>
))}
</tbody>
</table>
</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}>
<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(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>
<MetricCard
isLoading={isLoadingActivity}
className={cn(warnBlockedQueries && 'bg-destructive-200 border-destructive-400')}
>
<MetricCardHeader>
<MetricCardLabel
className={cn(warnBlockedQueries && '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 a slow transaction, a long-running migration, or a stuck
idle-in-transaction session.
</p>
</>
}
>
Blocked queries
</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue
className={cn('space-x-1', warnBlockedQueries && 'text-destructive')}
>
<span>{blockedQueries.length}</span>
{warnBlockedQueries && longestBlockedQuery && (
<>
<span className="text-foreground-light text-xs">·</span>
<span
role="button"
tabIndex={0}
className={cn(
'text-foreground-light text-xs cursor-pointer transition-all normal-nums',
'hover:text-foreground hover:underline',
'focus:text-foreground focus:underline'
)}
onClick={() => onSelectLongestBlocked()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onSelectLongestBlocked()
}
}}
>
PID {longestBlockedQuery.activity.pid} blocked for{' '}
<span className="tabular-nums">
{formatDuration(longestBlockedQuery.duration * 1000, 0)}
</span>
</span>
</>
)}
</MetricCardValue>
</MetricCardContent>
</MetricCard>
<MetricCard
isLoading={isLoadingActivity}
className={cn(warnTopBlocker && 'bg-destructive-200 border-destructive-400')}
>
<MetricCardHeader>
<MetricCardLabel
className={cn(warnTopBlocker && 'text-foreground')}
tooltip="The query blocking the most other queries"
>
Top blocker
</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue
className={cn(
'space-x-1',
queryBlockingTheMostQueries === null
? 'text-foreground-lighter'
: warnTopBlocker
? 'text-destructive'
: 'text-foreground'
)}
>
{queryBlockingTheMostQueries === null ? (
'-'
) : (
<>
<span
role="button"
tabIndex={0}
className="normal-nums cursor-pointer hover:underline focus:underline"
onClick={() => onSelectTopBlocker()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onSelectTopBlocker()
}
}}
>
PID: {queryBlockingTheMostQueries?.activity.pid}
</span>
<span className="text-foreground-light text-xs">·</span>
<span
className={cn(
'text-xs',
warnTopBlocker ? 'text-foreground-light' : 'text-foreground-lighter'
)}
>
Blocking {queryBlockingTheMostQueries?.count} other{' '}
{queryBlockingTheMostQueries?.count > 1 ? 'queries' : 'query'}
</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-1',
longestRunningQuery === null
? 'text-foreground-lighter'
: warnLongestRunningQuery
? 'text-warning'
: 'text-foreground'
)}
>
{longestRunningQuery === null ? (
'-'
) : (
<>
<span
role="button"
tabIndex={0}
className="normal-nums hover:underline focus:underline cursor-pointer"
onClick={() => onSelectLongestRunning()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onSelectLongestRunning()
}
}}
>
PID: {longestRunningQuery.activity.pid}
</span>
<span className="text-foreground-lighter text-sm">·</span>
<span
className={cn(
'text-sm',
warnLongestRunningQuery
? 'text-foreground-light'
: 'text-foreground-lighter'
)}
>
{formatDuration(longestRunningQuery.duration * 1000, 0)}
</span>
</>
)}
</MetricCardValue>
</MetricCardContent>
</MetricCard>
</div>
</div>
</div>
)
}