Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { BarChart, Shield } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'

import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import {
createLintSummaryPrompt,
LintCategoryBadge,
lintInfoMap,
} from 'components/interfaces/Linter/Linter.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import {
AiIconAnimation,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Sheet,
SheetContent,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { Row } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import LintDetail from 'components/interfaces/Linter/LintDetail'

export const AdvisorSection = () => {
const { ref: projectRef } = useParams()
const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef })
const snap = useAiAssistantStateSnapshot()

const [selectedLint, setSelectedLint] = useState<Lint | null>(null)

const errorLints: Lint[] = useMemo(() => {
return lints?.filter((lint) => lint.level === LINTER_LEVELS.ERROR) ?? []
}, [lints])

const totalErrors = errorLints.length

const titleContent = useMemo(() => {
if (totalErrors === 0) return <h2>Assistant found no issues</h2>
const issuesText = totalErrors === 1 ? 'issue' : 'issues'
const numberDisplay = totalErrors.toString()
return (
<h2>
Assistant found {numberDisplay} {issuesText}
</h2>
)
}, [totalErrors])

const handleAskAssistant = useCallback(() => {
snap.toggleAssistant()
}, [snap])

const handleCardClick = useCallback((lint: Lint) => {
setSelectedLint(lint)
}, [])

return (
<div>
{isLoadingLints ? (
<ShimmeringLoader className="w-96 mb-6" />
) : (
<div className="flex justify-between items-center mb-6">
{titleContent}
<Button type="default" icon={<AiIconAnimation />} onClick={handleAskAssistant}>
Ask Assistant
</Button>
</div>
)}
{isLoadingLints ? (
<div className="flex flex-col p-4 gap-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : errorLints.length > 0 ? (
<>
<Row columns={[3, 2, 1]}>
{errorLints.map((lint) => {
return (
<Card
key={lint.cache_key}
className="h-full flex flex-col items-stretch cursor-pointer"
onClick={() => {
handleCardClick(lint)
}}
>
<CardHeader className="border-b-0 shrink-0 flex flex-row gap-2 space-y-0 justify-between items-center">
<div className="flex flex-row items-center gap-3">
{lint.categories[0] === 'SECURITY' ? (
<Shield size={16} strokeWidth={1.5} className="text-foreground-muted" />
) : (
<BarChart size={16} strokeWidth={1.5} className="text-foreground-muted" />
)}
<CardTitle className="text-foreground-light">{lint.categories[0]}</CardTitle>
</div>
<ButtonTooltip
type="text"
className="w-7 h-7"
icon={<AiIconAnimation size={16} />}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: createLintSummaryPrompt(lint),
})
}}
tooltip={{
content: { side: 'bottom', text: 'Help me fix this issue' },
}}
/>
</CardHeader>
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
{lint.detail ? lint.detail.substring(0, 100) : lint.title}
{lint.detail && lint.detail.length > 100 && '...'}
</CardContent>
</Card>
)
})}
</Row>
<Sheet open={selectedLint !== null} onOpenChange={() => setSelectedLint(null)}>
<SheetContent>
{selectedLint && (
<>
<SheetHeader>
<div className="flex items-center gap-4">
<SheetTitle>
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title ??
'Unknown'}
</SheetTitle>
<LintCategoryBadge category={selectedLint.categories[0]} />
</div>
</SheetHeader>
<SheetSection>
{selectedLint && projectRef && (
<LintDetail
lint={selectedLint}
projectRef={projectRef!}
onAskAssistant={() => setSelectedLint(null)}
/>
)}
</SheetSection>
</>
)}
</SheetContent>
</Sheet>
</>
) : (
<Card className="bg-transparent">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16">
<Shield size={20} strokeWidth={1.5} className="text-foreground-muted" />
<p className="text-sm text-foreground-light text-center">
No security or performance errors found
</p>
</CardContent>
</Card>
)}
</div>
)
}
15 changes: 10 additions & 5 deletions apps/studio/components/interfaces/HomeNew/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { AdvisorSection } from './AdvisorSection'

export const HomeV2 = () => {
const { ref, enableBranching } = useParams()
Expand Down Expand Up @@ -100,11 +101,15 @@ export const HomeV2 = () => {
)}
strategy={verticalListSortingStrategy}
>
{sectionOrder.map((id) => (
<SortableSection key={id} id={id}>
{id}
</SortableSection>
))}
{sectionOrder.map((id) => {
if (id === 'advisor') {
return (
<SortableSection key={id} id={id}>
<AdvisorSection />
</SortableSection>
)
}
})}
</SortableContext>
</DndContext>
</ScaffoldSection>
Expand Down
73 changes: 73 additions & 0 deletions apps/studio/components/interfaces/Linter/LintDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'

import { createLintSummaryPrompt, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import { EntityTypeIcon, LintCTA, LintCategoryBadge, LintEntity } from './Linter.utils'
import { Lint } from 'data/lint/lint-query'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Button } from 'ui'
import { ExternalLink } from 'lucide-react'

interface LintDetailProps {
lint: Lint
projectRef: string
onAskAssistant?: () => void
}

const LintDetail = ({ lint, projectRef, onAskAssistant }: LintDetailProps) => {
const snap = useAiAssistantStateSnapshot()

return (
<div>
<h3 className="text-sm mb-2">Entity</h3>
<div className="flex items-center gap-1 px-2 py-0.5 bg-surface-200 border rounded-lg text-sm mb-6 w-fit">
<EntityTypeIcon type={lint.metadata?.type} />
<LintEntity metadata={lint.metadata} />
</div>

<h3 className="text-sm mb-2">Issue</h3>
<ReactMarkdown className="leading-6 text-sm text-foreground-light mb-6">
{lint.detail.replace(/\\`/g, '`')}
</ReactMarkdown>
<h3 className="text-sm mb-2">Description</h3>
<ReactMarkdown className="text-sm text-foreground-light mb-6">
{lint.description.replace(/\\`/g, '`')}
</ReactMarkdown>

<h3 className="text-sm mb-2">Resolve</h3>
<div className="flex items-center gap-2">
<Button
icon={<AiIconAnimation className="scale-75 w-3 h-3" />}
onClick={() => {
onAskAssistant?.()
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: createLintSummaryPrompt(lint),
})
}}
>
Ask Assistant
</Button>
<LintCTA title={lint.name} projectRef={projectRef} metadata={lint.metadata} />
<Button asChild type="text">
<Link
href={
lintInfoMap.find((item) => item.name === lint.name)?.docsLink ||
'https://supabase.com/docs/guides/database/database-linter'
}
target="_blank"
rel="noreferrer"
className="no-underline"
>
<span className="flex items-center gap-2">
Learn more <ExternalLink size={14} />
</span>
</Link>
</Button>
</div>
</div>
)
}

export default LintDetail
10 changes: 1 addition & 9 deletions apps/studio/components/interfaces/Linter/LintPageTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,10 @@ import { useRouter } from 'next/router'
interface LintPageTabsProps {
currentTab: string
setCurrentTab: (value: LINTER_LEVELS) => void
setSelectedLint: (value: Lint | null) => void
isLoading: boolean
activeLints: Lint[]
}
const LintPageTabs = ({
currentTab,
setCurrentTab,
setSelectedLint,
isLoading,
activeLints,
}: LintPageTabsProps) => {
const LintPageTabs = ({ currentTab, setCurrentTab, isLoading, activeLints }: LintPageTabsProps) => {
const router = useRouter()

const warnLintsCount = activeLints.filter((x) => x.level === 'WARN').length
Expand Down Expand Up @@ -73,7 +66,6 @@ const LintPageTabs = ({
defaultValue={currentTab}
onValueChange={(value) => {
setCurrentTab(value as LINTER_LEVELS)
setSelectedLint(null)
const { sort, search, ...rest } = router.query
router.push({ ...router, query: { ...rest, preset: value, id: null } })
}}
Expand Down
20 changes: 20 additions & 0 deletions apps/studio/components/interfaces/Linter/Linter.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,23 @@ export const NoIssuesFound = ({ level }: { level: string }) => {
</div>
)
}

export const createLintSummaryPrompt = (lint: Lint) => {
const title = lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title
const entity =
(lint.metadata &&
(lint.metadata.entity ||
(lint.metadata.schema &&
lint.metadata.name &&
`${lint.metadata.schema}.${lint.metadata.name}`))) ||
'N/A'
const schema = lint.metadata?.schema ?? 'N/A'
const issue = lint.detail ? lint.detail.replace(/\\`/g, '`') : 'N/A'
const description = lint.description ? lint.description.replace(/\\`/g, '`') : 'N/A'
return `Summarize the issue and suggest fixes for the following lint item:
Title: ${title}
Entity: ${entity}
Schema: ${schema}
Issue Details: ${issue}
Description: ${description}`
}
Loading
Loading