AdvisorWidget.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { useParams } from 'common'
  2. import { Activity, ExternalLink, Shield } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useCallback, useMemo, useState } from 'react'
  5. import {
  6. Card,
  7. CardContent,
  8. CardHeader,
  9. CardTitle,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableHead,
  14. TableHeader,
  15. TableRow,
  16. Tabs_Shadcn_ as Tabs,
  17. TabsContent_Shadcn_ as TabsContent,
  18. TabsList_Shadcn_ as TabsList,
  19. TabsTrigger_Shadcn_ as TabsTrigger,
  20. } from 'ui'
  21. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  22. import { useQueryPerformanceQuery } from '../QueryPerformance/useQueryPerformanceQuery'
  23. import { LINTER_LEVELS } from '@/components/interfaces/Linter/Linter.constants'
  24. import {
  25. createLintSummaryPrompt,
  26. EntityTypeIcon,
  27. } from '@/components/interfaces/Linter/Linter.utils'
  28. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  29. import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
  30. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  31. import { Lint, useProjectLintsQuery } from '@/data/lint/lint-query'
  32. import { useTrack } from '@/lib/telemetry/track'
  33. import { useAdvisorStateSnapshot } from '@/state/advisor-state'
  34. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  35. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  36. interface SlowQuery {
  37. rolname: string
  38. mean_time: number
  39. calls: number
  40. query: string
  41. }
  42. export const AdvisorWidget = () => {
  43. const { ref: projectRef } = useParams()
  44. const [selectedTab, setSelectedTab] = useState<'security' | 'performance'>('security')
  45. const { data: lints, isPending: isLoadingLints } = useProjectLintsQuery({ projectRef })
  46. const { data: slowestQueriesData, isLoading: isLoadingSlowestQueries } = useQueryPerformanceQuery(
  47. { preset: 'slowestExecutionTime' }
  48. )
  49. const snap = useAiAssistantStateSnapshot()
  50. const { openSidebar } = useSidebarManagerSnapshot()
  51. const { setSelectedItem } = useAdvisorStateSnapshot()
  52. const track = useTrack()
  53. const securityLints = useMemo(
  54. () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')),
  55. [lints]
  56. )
  57. const performanceLints = useMemo(
  58. () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('PERFORMANCE')),
  59. [lints]
  60. )
  61. const securityErrorCount = securityLints.filter(
  62. (lint: Lint) => lint.level === LINTER_LEVELS.ERROR
  63. ).length
  64. const securityWarningCount = securityLints.filter(
  65. (lint: Lint) => lint.level === LINTER_LEVELS.WARN
  66. ).length
  67. const performanceErrorCount = performanceLints.filter(
  68. (lint: Lint) => lint.level === LINTER_LEVELS.ERROR
  69. ).length
  70. const performanceWarningCount = performanceLints.filter(
  71. (lint: Lint) => lint.level === LINTER_LEVELS.WARN
  72. ).length
  73. const top5SlowestQueries = useMemo(
  74. () => ((slowestQueriesData ?? []) as SlowQuery[]).slice(0, 5),
  75. [slowestQueriesData]
  76. )
  77. const handleLintClick = useCallback(
  78. (lint: Lint) => {
  79. setSelectedItem(lint.cache_key, 'lint')
  80. openSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
  81. },
  82. [setSelectedItem, openSidebar]
  83. )
  84. const totalIssues =
  85. securityErrorCount + securityWarningCount + performanceErrorCount + performanceWarningCount
  86. const hasErrors = securityErrorCount > 0 || performanceErrorCount > 0
  87. const hasWarnings = securityWarningCount > 0 || performanceWarningCount > 0
  88. let titleContent: React.ReactNode
  89. if (totalIssues === 0) {
  90. titleContent = <h2>No issues available</h2>
  91. } else {
  92. const issuesText = totalIssues === 1 ? 'issue' : 'issues'
  93. const numberDisplay = totalIssues.toString()
  94. let attentionClassName = ''
  95. if (hasErrors) {
  96. attentionClassName = 'text-destructive'
  97. } else if (hasWarnings) {
  98. attentionClassName = 'text-warning'
  99. }
  100. titleContent = (
  101. <h2>
  102. {numberDisplay} {issuesText} need
  103. {totalIssues === 1 ? 's' : ''} <span className={attentionClassName}>attention</span>
  104. </h2>
  105. )
  106. }
  107. const renderLintTabContent = (
  108. title: string,
  109. lints: Lint[],
  110. errorCount: number,
  111. warningCount: number,
  112. isLoading: boolean
  113. ) => {
  114. const topIssues = lints
  115. .filter((lint) => lint.level === LINTER_LEVELS.ERROR || lint.level === LINTER_LEVELS.WARN)
  116. .sort((a, _b) => (a.level === LINTER_LEVELS.ERROR ? -1 : 1))
  117. return (
  118. <div className="h-full">
  119. {isLoading && (
  120. <div className="flex flex-col p-4 gap-2">
  121. <ShimmeringLoader />
  122. <ShimmeringLoader className="w-3/4" />
  123. <ShimmeringLoader className="w-1/2" />
  124. </div>
  125. )}
  126. {!isLoading && (errorCount > 0 || warningCount > 0) && (
  127. <ul>
  128. {topIssues.map((lint) => {
  129. const lintText = lint.detail ? lint.detail : lint.title
  130. return (
  131. <li
  132. key={lint.cache_key}
  133. className="text-sm w-full border-b my-0 last:border-b-0 group px-4 "
  134. >
  135. <div className="flex items-center justify-between w-full group">
  136. <button
  137. onClick={() => handleLintClick(lint)}
  138. className="flex items-center gap-2 transition truncate flex-1 min-w-0 py-3 text-left"
  139. >
  140. <EntityTypeIcon type={lint.metadata?.type} />
  141. <p className="flex-1 font-mono text-xs leading-6 text-xs text-foreground-light group-hover:text-foreground truncate">
  142. {lintText.replace(/\\`/g, '`')}
  143. </p>
  144. </button>
  145. <div
  146. onClick={(e) => {
  147. e.stopPropagation()
  148. e.preventDefault()
  149. }}
  150. className="opacity-0 group-hover:opacity-100"
  151. >
  152. <AiAssistantDropdown
  153. label="Ask Assistant"
  154. iconOnly
  155. tooltip="Help me fix this issue"
  156. buildPrompt={() => createLintSummaryPrompt(lint)}
  157. onOpenAssistant={() => {
  158. openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
  159. snap.newChat({
  160. name: 'Summarize lint',
  161. initialInput: createLintSummaryPrompt(lint),
  162. })
  163. track('advisor_assistant_button_clicked', {
  164. origin: 'homepage',
  165. advisorCategory: lint.categories[0],
  166. advisorType: lint.name,
  167. advisorLevel: lint.level,
  168. })
  169. }}
  170. telemetrySource="advisor_widget"
  171. type="text"
  172. className="px-1 w-7"
  173. />
  174. </div>
  175. </div>
  176. </li>
  177. )
  178. })}
  179. </ul>
  180. )}
  181. {!isLoading && errorCount === 0 && warningCount === 0 && (
  182. <div className="flex-1 flex flex-col h-full items-center justify-center gap-2">
  183. <Shield size={20} strokeWidth={1.5} className="text-foreground-muted" />
  184. <p className="text-sm text-foreground-light">No {title.toLowerCase()} issues found</p>
  185. </div>
  186. )}
  187. </div>
  188. )
  189. }
  190. return (
  191. <div className="@container">
  192. {isLoadingLints ? (
  193. <ShimmeringLoader className="w-96 mb-6" />
  194. ) : (
  195. <div className="flex justify-between items-center mb-6">{titleContent}</div>
  196. )}
  197. <div className="grid grid-cols-1 @xl:grid-cols-2 gap-4">
  198. <Card className="h-80">
  199. <Tabs value={selectedTab} className="h-full flex flex-col">
  200. <CardHeader className="h-10 py-0 pl-4 pr-2 flex flex-row items-center justify-between flex-0">
  201. <TabsList className="flex justify-start rounded-none gap-x-4 border-b-0 mt-0! pt-0">
  202. <TabsTrigger
  203. value="security"
  204. onClick={() => setSelectedTab('security')}
  205. className="flex items-center gap-2 text-xs py-3 border-b font-mono uppercase"
  206. >
  207. Security{' '}
  208. {securityErrorCount + securityWarningCount > 0 && (
  209. <div className="rounded-sm bg-warning text-warning-100 px-1">
  210. {securityErrorCount + securityWarningCount}
  211. </div>
  212. )}
  213. </TabsTrigger>
  214. <TabsTrigger
  215. value="performance"
  216. onClick={() => setSelectedTab('performance')}
  217. className="flex items-center gap-2 text-xs py-3 border-b font-mono uppercase"
  218. >
  219. Performance{' '}
  220. {performanceErrorCount + performanceWarningCount > 0 && (
  221. <div className="rounded-sm bg-warning text-warning-100 px-1">
  222. {performanceErrorCount + performanceWarningCount}
  223. </div>
  224. )}
  225. </TabsTrigger>
  226. </TabsList>
  227. <ButtonTooltip
  228. asChild
  229. type="text"
  230. className="mt-0! w-7"
  231. icon={<ExternalLink />}
  232. tooltip={{
  233. content: {
  234. side: 'bottom',
  235. text: `Open ${selectedTab} Advisor`,
  236. className: 'capitalize',
  237. },
  238. }}
  239. >
  240. <Link
  241. href={`/project/${projectRef}/advisors/${selectedTab}`}
  242. aria-label={`Open ${selectedTab} advisor`}
  243. />
  244. </ButtonTooltip>
  245. </CardHeader>
  246. <CardContent className="p-0! mt-0 flex-1 overflow-y-auto">
  247. <TabsContent value="security" className="p-0 mt-0 h-full">
  248. {renderLintTabContent(
  249. 'Security',
  250. securityLints,
  251. securityErrorCount,
  252. securityWarningCount,
  253. isLoadingLints
  254. )}
  255. </TabsContent>
  256. <TabsContent value="performance" className="p-0 mt-0 h-full">
  257. {renderLintTabContent(
  258. 'Performance',
  259. performanceLints,
  260. performanceErrorCount,
  261. performanceWarningCount,
  262. isLoadingLints
  263. )}
  264. </TabsContent>
  265. </CardContent>
  266. </Tabs>
  267. </Card>
  268. <Card className="h-80 flex flex-col">
  269. <CardHeader className="h-10 flex-row items-center justify-between py-0 pl-4 pr-2">
  270. <CardTitle>Slow Queries</CardTitle>
  271. <ButtonTooltip
  272. asChild
  273. type="text"
  274. className="mt-0! w-7"
  275. icon={<ExternalLink />}
  276. tooltip={{
  277. content: {
  278. side: 'bottom',
  279. text: `Open Query Performance Advisor`,
  280. },
  281. }}
  282. >
  283. <Link
  284. href={`/project/${projectRef}/reports/query-performance`}
  285. aria-label="Open Query Performance Advisor"
  286. />
  287. </ButtonTooltip>
  288. </CardHeader>
  289. <CardContent className="p-0! flex-1 overflow-y-auto">
  290. {isLoadingSlowestQueries ? (
  291. <div className="space-y-2 p-4">
  292. <ShimmeringLoader />
  293. <ShimmeringLoader className="w-3/4" />
  294. <ShimmeringLoader className="w-1/2" />
  295. <ShimmeringLoader className="w-3/4" />
  296. <ShimmeringLoader className="w-1/2" />
  297. </div>
  298. ) : top5SlowestQueries.length === 0 ? (
  299. <div className="flex-1 flex flex-col h-full items-center justify-center gap-2">
  300. <Activity strokeWidth={1.5} size={20} className="text-foreground-muted" />
  301. <p className="text-sm text-foreground-light">
  302. No slow queries found in the selected period
  303. </p>
  304. </div>
  305. ) : (
  306. <Table className="text-xs font-mono max-w-full">
  307. <TableHeader>
  308. <TableRow>
  309. <TableHead className="text-foreground-lighter truncate py-2 h-auto">
  310. Query
  311. </TableHead>
  312. <TableHead className="text-foreground-lighter truncate py-2 h-auto">
  313. Avg time
  314. </TableHead>
  315. <TableHead className="text-foreground-lighter truncate py-2 h-auto">
  316. Calls
  317. </TableHead>
  318. </TableRow>
  319. </TableHeader>
  320. <TableBody>
  321. {/* Added explicit types for map parameters */}
  322. {top5SlowestQueries.map((query: SlowQuery, i: number) => (
  323. <TableRow key={i} className="py-2">
  324. <TableCell className="font-mono truncate max-w-xs">{query.query}</TableCell>
  325. <TableCell className="font-mono truncate max-w-xs">
  326. {typeof query.mean_time === 'number'
  327. ? `${(query.mean_time / 1000).toFixed(2)}s`
  328. : 'N/A'}
  329. </TableCell>
  330. <TableCell className="font-mono truncate max-w-xs">{query.calls}</TableCell>
  331. </TableRow>
  332. ))}
  333. </TableBody>
  334. </Table>
  335. )}
  336. </CardContent>
  337. </Card>
  338. </div>
  339. </div>
  340. )
  341. }