LintDetail.tsx112 lines · main
| 1 | import { ExternalLink } from 'lucide-react' |
| 2 | import Link from 'next/link' |
| 3 | import { Button } from 'ui' |
| 4 | |
| 5 | import { Markdown } from '../Markdown' |
| 6 | import { asGraphqlExposureLint, GraphqlExposureCallout } from './GraphqlExposureLintCTA' |
| 7 | import { EntityTypeIcon, LintCTA, LintEntity } from './Linter.utils' |
| 8 | import { createLintSummaryPrompt, lintInfoMap } from '@/components/interfaces/Linter/Linter.utils' |
| 9 | import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' |
| 10 | import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown' |
| 11 | import { Lint } from '@/data/lint/lint-query' |
| 12 | import { DOCS_URL } from '@/lib/constants' |
| 13 | import { useTrack } from '@/lib/telemetry/track' |
| 14 | import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' |
| 15 | import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' |
| 16 | |
| 17 | interface LintDetailProps { |
| 18 | lint: Lint |
| 19 | projectRef: string |
| 20 | onAskAssistant?: () => void |
| 21 | onAfterAction?: () => void |
| 22 | } |
| 23 | |
| 24 | export const LintDetail = ({ |
| 25 | lint, |
| 26 | projectRef, |
| 27 | onAskAssistant, |
| 28 | onAfterAction, |
| 29 | }: LintDetailProps) => { |
| 30 | const track = useTrack() |
| 31 | const snap = useAiAssistantStateSnapshot() |
| 32 | const { openSidebar } = useSidebarManagerSnapshot() |
| 33 | const isGraphqlExposureLint = !!asGraphqlExposureLint(lint.name) |
| 34 | |
| 35 | const handleAskAssistant = () => { |
| 36 | track('advisor_assistant_button_clicked', { |
| 37 | origin: 'lint_detail', |
| 38 | advisorCategory: lint.categories[0], |
| 39 | advisorType: lint.name, |
| 40 | advisorLevel: lint.level, |
| 41 | }) |
| 42 | |
| 43 | onAskAssistant?.() |
| 44 | openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) |
| 45 | snap.newChat({ |
| 46 | name: 'Summarize lint', |
| 47 | initialMessage: createLintSummaryPrompt(lint), |
| 48 | }) |
| 49 | } |
| 50 | |
| 51 | const buildPromptForCopy = () => { |
| 52 | return createLintSummaryPrompt(lint) |
| 53 | } |
| 54 | |
| 55 | return ( |
| 56 | <div> |
| 57 | <h3 className="text-sm mb-2">Entity</h3> |
| 58 | <div className="flex items-center gap-1 px-2 py-0.5 bg-surface-200 border rounded-lg text-sm mb-6 w-fit"> |
| 59 | <EntityTypeIcon type={lint.metadata?.type} /> |
| 60 | <LintEntity metadata={lint.metadata} /> |
| 61 | </div> |
| 62 | |
| 63 | <h3 className="text-sm mb-2">Issue</h3> |
| 64 | <Markdown className="leading-6 text-sm text-foreground-light mb-6"> |
| 65 | {lint.detail.replace(/\\`/g, '`')} |
| 66 | </Markdown> |
| 67 | <h3 className="text-sm mb-2">Description</h3> |
| 68 | <Markdown className="text-sm text-foreground-light mb-6"> |
| 69 | {lint.description.replace(/\\`/g, '`')} |
| 70 | </Markdown> |
| 71 | |
| 72 | {isGraphqlExposureLint && ( |
| 73 | <div className="mb-4"> |
| 74 | <GraphqlExposureCallout projectRef={projectRef} /> |
| 75 | </div> |
| 76 | )} |
| 77 | |
| 78 | <h3 className="text-sm mb-2">Resolve</h3> |
| 79 | <div className="flex flex-wrap items-center gap-2"> |
| 80 | <AiAssistantDropdown |
| 81 | label="Ask Assistant" |
| 82 | buildPrompt={buildPromptForCopy} |
| 83 | onOpenAssistant={handleAskAssistant} |
| 84 | telemetrySource="lint_detail" |
| 85 | /> |
| 86 | |
| 87 | <LintCTA |
| 88 | title={lint.name} |
| 89 | projectRef={projectRef} |
| 90 | metadata={lint.metadata} |
| 91 | onAfterAction={onAfterAction} |
| 92 | /> |
| 93 | |
| 94 | <Button asChild type="text"> |
| 95 | <Link |
| 96 | href={ |
| 97 | lintInfoMap.find((item) => item.name === lint.name)?.docsLink || |
| 98 | `${DOCS_URL}/guides/database/database-linter` |
| 99 | } |
| 100 | target="_blank" |
| 101 | rel="noreferrer" |
| 102 | className="no-underline" |
| 103 | > |
| 104 | <span className="flex items-center gap-2"> |
| 105 | Learn more <ExternalLink size={14} /> |
| 106 | </span> |
| 107 | </Link> |
| 108 | </Button> |
| 109 | </div> |
| 110 | </div> |
| 111 | ) |
| 112 | } |