mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15: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 -->
180 lines
6.4 KiB
TypeScript
180 lines
6.4 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useDebounce } from '@uidotdev/usehooks'
|
|
import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
|
|
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import {
|
|
InnerSideBarFilters,
|
|
InnerSideBarFilterSearchInput,
|
|
InnerSideBarFilterSortDropdown,
|
|
InnerSideBarFilterSortDropdownItem,
|
|
} from 'ui-patterns/InnerSideMenu'
|
|
|
|
import { SearchList } from './SQLEditorNavV2/SearchList'
|
|
import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useLocalStorage } from '@/hooks/misc/useLocalStorage'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useProfile } from '@/lib/profile'
|
|
import { getAppStateSnapshot } from '@/state/app-state'
|
|
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
|
|
|
|
export const SQLEditorMenu = () => {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
const { profile } = useProfile()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const snapV2 = useSqlEditorV2StateSnapshot()
|
|
|
|
const topForPostgres = useFlag('topForPostgres')
|
|
|
|
const [search, setSearch] = useState('')
|
|
const [showSearch, setShowSearch] = useState(false)
|
|
const [sort, setSort] = useLocalStorage<'name' | 'inserted_at'>(
|
|
LOCAL_STORAGE_KEYS.SQL_EDITOR_SORT(ref ?? ''),
|
|
'inserted_at'
|
|
)
|
|
|
|
const appState = getAppStateSnapshot()
|
|
const debouncedSearch = useDebounce(search, 500)
|
|
|
|
const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
|
|
PermissionAction.CREATE,
|
|
'user_content',
|
|
{
|
|
resource: { type: 'sql', owner_id: profile?.id },
|
|
subject: { id: profile?.id },
|
|
}
|
|
)
|
|
|
|
const createNewFolder = () => {
|
|
if (!ref) return console.error('Project ref is required')
|
|
setSearch('')
|
|
setShowSearch(false)
|
|
snapV2.addNewFolder({ projectRef: ref })
|
|
}
|
|
|
|
const handleNewQuery = async () => {
|
|
if (!ref) return console.error('Project ref is required')
|
|
if (!project) return console.error('Project is required')
|
|
if (!profile) return console.error('Profile is required')
|
|
if (!canCreateSQLSnippet) {
|
|
return toast('Your queries will not be saved as you do not have sufficient permissions')
|
|
}
|
|
try {
|
|
router.push(`/project/${ref}/sql/new?skip=true`)
|
|
setSearch('')
|
|
setShowSearch(false)
|
|
} catch (error: any) {
|
|
toast.error(`Failed to create new query: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
setShowSearch(debouncedSearch.length > 0)
|
|
}, [debouncedSearch])
|
|
|
|
return (
|
|
<div className="h-full flex flex-col justify-between">
|
|
<div className="flex flex-col gap-y-4 grow">
|
|
<div className="mt-4 mx-4 flex items-center justify-between gap-x-2">
|
|
<InnerSideBarFilters className="w-full p-0 gap-0">
|
|
<InnerSideBarFilterSearchInput
|
|
name="search-queries"
|
|
placeholder="Search queries..."
|
|
aria-labelledby="Search queries"
|
|
value={search}
|
|
onChange={(e) => {
|
|
const value = e.target.value
|
|
setSearch(value)
|
|
if (value.length === 0) setShowSearch(false)
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.code === 'Escape') {
|
|
setSearch('')
|
|
setShowSearch(false)
|
|
}
|
|
}}
|
|
>
|
|
{showSearch ? (
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
className="absolute right-1 top-[.4rem] md:top-[.3rem] transition-colors text-foreground-light hover:text-foreground"
|
|
onClick={() => {
|
|
setSearch('')
|
|
setShowSearch(false)
|
|
}}
|
|
>
|
|
<X size={18} />
|
|
</TooltipTrigger>
|
|
<TooltipContent>Clear search</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
<InnerSideBarFilterSortDropdown
|
|
value={sort}
|
|
onValueChange={(value: any) => setSort(value)}
|
|
>
|
|
<InnerSideBarFilterSortDropdownItem key="name" value="name">
|
|
Alphabetical
|
|
</InnerSideBarFilterSortDropdownItem>
|
|
<InnerSideBarFilterSortDropdownItem key="inserted_at" value="inserted_at">
|
|
Created At
|
|
</InnerSideBarFilterSortDropdownItem>
|
|
</InnerSideBarFilterSortDropdown>
|
|
)}
|
|
</InnerSideBarFilterSearchInput>
|
|
</InnerSideBarFilters>
|
|
<DropdownMenu>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
data-testid="sql-editor-new-query-button"
|
|
variant="default"
|
|
icon={<Plus className="text-foreground" />}
|
|
className="w-[26px]"
|
|
aria-label="Create a new query"
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Create a new query</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent align="end" side="bottom" className="w-48">
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => handleNewQuery()}>
|
|
<FilePlus size={14} />
|
|
Create a new snippet
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => createNewFolder()}>
|
|
<FolderPlus size={14} />
|
|
Create a new folder
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{showSearch ? <SearchList search={debouncedSearch} /> : <SQLEditorNav sort={sort} />}
|
|
</div>
|
|
|
|
{!topForPostgres && (
|
|
<div className="p-4 border-t sticky bottom-0 bg-studio">
|
|
<Button block variant="default" onClick={() => appState.setOnGoingQueriesPanelOpen(true)}>
|
|
View running queries
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|