OngoingQueriesPanel.tsx 6.9 KB

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