ReportBlock.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { acceptUntrustedSql } from '@supabase/pg-meta'
  2. import { useQuery } from '@tanstack/react-query'
  3. import { useParams } from 'common'
  4. import { X } from 'lucide-react'
  5. import { useEffect, useState } from 'react'
  6. import { toast } from 'sonner'
  7. import { DEPRECATED_REPORTS } from '../Reports.constants'
  8. import { ChartBlock } from './ChartBlock'
  9. import { DeprecatedChartBlock } from './DeprecatedChartBlock'
  10. import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
  11. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  12. import { DEFAULT_CHART_CONFIG, QueryBlock } from '@/components/ui/QueryBlock/QueryBlock'
  13. import { AnalyticsInterval } from '@/data/analytics/constants'
  14. import { useContentIdQuery } from '@/data/content/content-id-query'
  15. import { usePrimaryDatabase } from '@/data/read-replicas/replicas-query'
  16. import { executeSql } from '@/data/sql/execute-sql-query'
  17. import { sqlKeys } from '@/data/sql/keys'
  18. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  19. import type { Dashboards, SqlSnippets } from '@/types'
  20. interface ReportBlockProps {
  21. item: Dashboards.Chart
  22. startDate: string
  23. endDate: string
  24. interval: AnalyticsInterval
  25. disableUpdate: boolean
  26. isRefreshing: boolean
  27. onRemoveChart: ({ metric }: { metric: { key: string } }) => void
  28. onUpdateChart: ({
  29. chart,
  30. chartConfig,
  31. }: {
  32. chart?: Partial<Dashboards.Chart>
  33. chartConfig?: Partial<ChartConfig>
  34. }) => void
  35. }
  36. export const ReportBlock = ({
  37. item,
  38. startDate,
  39. endDate,
  40. interval,
  41. disableUpdate,
  42. isRefreshing,
  43. onRemoveChart,
  44. onUpdateChart,
  45. }: ReportBlockProps) => {
  46. const { ref: projectRef } = useParams()
  47. const state = useDatabaseSelectorStateSnapshot()
  48. const [isWriteQuery, setIsWriteQuery] = useState(false)
  49. const isSnippet = item.attribute.startsWith('snippet_')
  50. const {
  51. data,
  52. error: contentError,
  53. isPending: isLoadingContent,
  54. } = useContentIdQuery(
  55. { projectRef, id: item.id },
  56. {
  57. enabled: isSnippet && !!item.id,
  58. refetchOnWindowFocus: false,
  59. refetchOnMount: false,
  60. refetchIntervalInBackground: false,
  61. retry: (failureCount: number, error) => {
  62. if (error.code === 404 || failureCount >= 2) return false
  63. return true
  64. },
  65. }
  66. )
  67. const sql = isSnippet ? (data?.content as SqlSnippets.Content)?.unchecked_sql : undefined
  68. const chartConfig = { ...DEFAULT_CHART_CONFIG, ...(item.chartConfig ?? {}) }
  69. const isDeprecatedChart = DEPRECATED_REPORTS.includes(item.attribute)
  70. const snippetMissing = contentError?.message.includes('Content not found')
  71. const { database: primaryDatabase } = usePrimaryDatabase({ projectRef })
  72. const readOnlyConnectionString = primaryDatabase?.connection_string_read_only
  73. const postgresConnectionString = primaryDatabase?.connectionString
  74. const {
  75. data: queryResult,
  76. error: executeSqlError,
  77. isPending: executeSqlLoading,
  78. refetch,
  79. } = useQuery({
  80. queryKey: sqlKeys.query(projectRef, [
  81. item.id,
  82. sql,
  83. readOnlyConnectionString,
  84. postgresConnectionString,
  85. ]),
  86. queryFn: async () => {
  87. if (!projectRef || !sql) return null
  88. const connectionString = readOnlyConnectionString ?? postgresConnectionString
  89. if (!connectionString) {
  90. toast.error('Unable to establish a database connection for this project.')
  91. return null
  92. }
  93. return executeSql({
  94. projectRef,
  95. connectionString,
  96. // acceptUntrustedSql is usually not allowed in an auto-run position,
  97. // but in this case we are explicitly allowing it because adding a block
  98. // to a report is an explicit user action.
  99. sql: acceptUntrustedSql(sql),
  100. })
  101. },
  102. enabled: !isLoadingContent && contentError == null,
  103. refetchOnWindowFocus: false,
  104. })
  105. const rows = queryResult?.result
  106. useEffect(() => {
  107. if (executeSqlError) {
  108. const errorMessage = String(executeSqlError).toLowerCase()
  109. const isReadOnlyError =
  110. errorMessage.includes('read-only transaction') ||
  111. errorMessage.includes('permission denied') ||
  112. errorMessage.includes('must be owner')
  113. if (isReadOnlyError) {
  114. setIsWriteQuery(true)
  115. }
  116. }
  117. }, [executeSqlError])
  118. useEffect(() => {
  119. if (isRefreshing) {
  120. refetch()
  121. }
  122. }, [isRefreshing, refetch])
  123. return (
  124. <>
  125. {isSnippet ? (
  126. <QueryBlock
  127. blockWriteQueries
  128. id={item.id}
  129. label={item.label}
  130. chartConfig={chartConfig}
  131. sql={sql}
  132. results={rows}
  133. initialHideSql={true}
  134. errorText={
  135. snippetMissing
  136. ? 'SQL snippet not found'
  137. : executeSqlError
  138. ? String(executeSqlError)
  139. : undefined
  140. }
  141. isExecuting={!contentError && executeSqlLoading}
  142. isWriteQuery={isWriteQuery}
  143. actions={
  144. <ButtonTooltip
  145. type="text"
  146. icon={<X />}
  147. className="w-7 h-7"
  148. onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
  149. tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
  150. />
  151. }
  152. onExecute={(_queryType) => {
  153. refetch()
  154. }}
  155. onUpdateChartConfig={onUpdateChart}
  156. onRemoveChart={() => onRemoveChart({ metric: { key: item.attribute } })}
  157. disabled={isLoadingContent || snippetMissing || !sql}
  158. />
  159. ) : isDeprecatedChart ? (
  160. <DeprecatedChartBlock
  161. attribute={item.attribute}
  162. label={`${item.label}${projectRef !== state.selectedDatabaseId ? (item.provider === 'infra-monitoring' ? ' of replica' : ' on project') : ''}`}
  163. actions={
  164. !disableUpdate ? (
  165. <ButtonTooltip
  166. type="text"
  167. icon={<X />}
  168. className="w-7 h-7"
  169. onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
  170. tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
  171. />
  172. ) : null
  173. }
  174. />
  175. ) : (
  176. <ChartBlock
  177. startDate={startDate}
  178. endDate={endDate}
  179. interval={interval}
  180. attribute={item.attribute}
  181. provider={item.provider}
  182. defaultChartStyle={item.chart_type}
  183. defaultLogScale={chartConfig?.logScale ?? false}
  184. maxHeight={176}
  185. label={`${item.label}${projectRef !== state.selectedDatabaseId ? (item.provider === 'infra-monitoring' ? ' of replica' : ' on project') : ''}`}
  186. actions={
  187. !disableUpdate ? (
  188. <ButtonTooltip
  189. type="text"
  190. icon={<X />}
  191. className="w-7 h-7"
  192. onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
  193. tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
  194. />
  195. ) : null
  196. }
  197. onUpdateChartConfig={onUpdateChart}
  198. />
  199. )}
  200. </>
  201. )
  202. }