OngoingQueriesPanel.tsx192 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { RefreshCw, StopCircle } from 'lucide-react'
4import { useEffect, useState } from 'react'
5import { toast } from 'sonner'
6import {
7 Button,
8 cn,
9 Sheet,
10 SheetContent,
11 SheetDescription,
12 SheetHeader,
13 SheetSection,
14 SheetTitle,
15 Tooltip,
16 TooltipContent,
17 TooltipTrigger,
18} from 'ui'
19import { CodeBlock } from 'ui-patterns/CodeBlock'
20import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
21
22import AlertError from '@/components/ui/AlertError'
23import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
24import { useQueryAbortMutation } from '@/data/sql/abort-query-mutation'
25import { useOngoingQueriesQuery } from '@/data/sql/ongoing-queries-query'
26import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
27import { useUrlState } from '@/hooks/ui/useUrlState'
28import { IS_PLATFORM } from '@/lib/constants'
29import { useAppStateSnapshot } from '@/state/app-state'
30import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
31import type { ResponseError } from '@/types'
32
33export const OngoingQueriesPanel = () => {
34 const [_, setParams] = useUrlState({ replace: true })
35 const { viewOngoingQueries } = useParams()
36 const { data: project } = useSelectedProjectQuery()
37 const state = useDatabaseSelectorStateSnapshot()
38 const appState = useAppStateSnapshot()
39 const [selectedId, setSelectedId] = useState<number>()
40
41 const { data: databases } = useReadReplicasQuery({ projectRef: project?.ref })
42 const database = (databases ?? []).find((db) => db.identifier === state.selectedDatabaseId)
43
44 const {
45 data,
46 error,
47 isError,
48 isPending: isLoadingOngoingQueries,
49 isFetching: isFetchingOngoingQueries,
50 refetch,
51 } = useOngoingQueriesQuery(
52 {
53 projectRef: project?.ref,
54 connectionString: database?.connectionString,
55 },
56 {
57 enabled: !IS_PLATFORM || (IS_PLATFORM && database?.connectionString !== undefined),
58 staleTime: 5000,
59 }
60 )
61 const queries = data ?? []
62
63 useEffect(() => {
64 if (viewOngoingQueries) {
65 appState.setOnGoingQueriesPanelOpen(true)
66 setParams({ viewOngoingQueries: undefined })
67 }
68 }, [viewOngoingQueries])
69
70 const { mutate: abortQuery, isPending } = useQueryAbortMutation({
71 onSuccess: () => {
72 toast.success(`Successfully aborted query (ID: ${selectedId})`)
73 setSelectedId(undefined)
74 },
75 })
76
77 const closePanel = () => {
78 setParams({ viewOngoingQueries: undefined })
79 appState.setOnGoingQueriesPanelOpen(false)
80 }
81
82 return (
83 <>
84 <Sheet open={appState.ongoingQueriesPanelOpen} onOpenChange={() => closePanel()}>
85 <SheetContent size="lg">
86 <SheetHeader>
87 <SheetTitle className="flex items-center gap-x-2">
88 Running queries on{' '}
89 {database?.identifier === project?.ref ? 'primary database' : 'read replica'}
90 <Button
91 type="default"
92 className="px-1.5"
93 loading={isLoadingOngoingQueries || isFetchingOngoingQueries}
94 icon={<RefreshCw />}
95 onClick={() => refetch()}
96 />
97 </SheetTitle>
98 <SheetDescription>
99 There {queries.length === 1 ? 'is' : 'are'}{' '}
100 <span className="text-foreground-light">{queries.length}</span> quer
101 {queries.length === 1 ? 'y' : 'ies'} currently running{' '}
102 {database?.identifier !== project?.ref ? `on replica ${database?.identifier}` : ''}
103 </SheetDescription>
104 </SheetHeader>
105 <div className="max-h-full h-full divide-y overflow-y-auto">
106 {isError && (
107 <div className="flex items-center justify-center h-full px-16">
108 <AlertError
109 subject="Failed to retrieve ongoing queries"
110 error={error as ResponseError}
111 />
112 </div>
113 )}
114 {queries.length === 0 && (
115 <div className="flex flex-col gap-y-2 items-center justify-center h-full text-foreground-light text-sm">
116 <span>
117 No queries are currently running on the{' '}
118 {database?.identifier !== project?.ref
119 ? `read replica ${database?.identifier}`
120 : (databases ?? []).length > 1
121 ? 'primary database'
122 : 'database'}
123 </span>
124 <Button
125 type="default"
126 loading={isLoadingOngoingQueries || isFetchingOngoingQueries}
127 icon={<RefreshCw />}
128 onClick={() => refetch()}
129 >
130 Refresh
131 </Button>
132 </div>
133 )}
134 {queries.map((query) => (
135 <SheetSection key={query.pid} className="flex justify-between gap-x-4">
136 <div className="flex flex-col gap-y-2 w-full">
137 <CodeBlock
138 hideLineNumbers
139 value={query.query}
140 language="sql"
141 className={cn(
142 'max-w-none max-h-52 w-full',
143 'bg-transparent! py-3! px-3.5! prose dark:prose-dark',
144 '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
145 )}
146 />
147 <div className="flex items-center gap-x-2">
148 <p className="text-foreground-light text-xs">PID: {query.pid}</p>
149 <p className="text-foreground-light text-xs">•</p>
150 <p className="text-foreground-light text-xs">
151 Started since: {dayjs(query.query_start).format('DD MMM YYYY HH:mm (ZZ)')}
152 </p>
153 </div>
154 </div>
155
156 <Tooltip>
157 <TooltipTrigger asChild>
158 <Button
159 type="warning"
160 className="px-1.5"
161 icon={<StopCircle />}
162 onClick={() => setSelectedId(query.pid)}
163 />
164 </TooltipTrigger>
165 <TooltipContent side="bottom">Abort query</TooltipContent>
166 </Tooltip>
167 </SheetSection>
168 ))}
169 </div>
170 </SheetContent>
171 </Sheet>
172
173 <ConfirmationModal
174 loading={isPending}
175 variant="warning"
176 title={`Confirm to abort this query? (ID: ${selectedId})`}
177 visible={selectedId !== undefined}
178 onCancel={() => setSelectedId(undefined)}
179 onConfirm={() => {
180 if (selectedId !== undefined)
181 abortQuery({
182 pid: selectedId,
183 projectRef: project?.ref,
184 connectionString: database?.connectionString,
185 })
186 }}
187 >
188 <p className="text-sm">This will force the query to stop running.</p>
189 </ConfirmationModal>
190 </>
191 )
192}