Skip to content

Commit ba05d2b

Browse files
committed
added search again button for both subpages
1 parent a6d98c3 commit ba05d2b

2 files changed

Lines changed: 209 additions & 7 deletions

File tree

src/components/package-inspector/purlPage.tsx

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { useState, useEffect } from 'react'
12
import Head from 'next/head'
23
import useSWR from 'swr'
34
import Link from 'next/link'
4-
import { ExternalLink } from 'lucide-react'
5+
import { useRouter } from 'next/router'
6+
import { ExternalLink, Search, X, ChevronDown } from 'lucide-react'
57
import { fetcher } from '@/lib/fetcher'
6-
import { extractPackageName } from '@/lib/utils'
8+
import { extractPackageName, cn } from '@/lib/utils'
79
import { PackageInspectResult } from '@/components/package-inspector/types'
810
import PackageHeroCard from '@/components/package-inspector/PackageHeroCard'
911
import ScoreCardChart from '@/components/package-inspector/ScoreCardChart'
@@ -12,11 +14,77 @@ import { Button } from '@/components/ui/button'
1214
import { Skeleton } from '@/components/ui/skeleton'
1315
import { Container } from '../ui/container'
1416

17+
const ECOSYSTEMS = [
18+
{ label: 'Alpine', value: 'alpine' },
19+
{ label: 'Bitnami', value: 'bitnami' },
20+
{ label: 'Crates.io', value: 'crates.io' },
21+
{ label: 'Debian', value: 'debian' },
22+
{ label: 'Git', value: 'git' },
23+
{ label: 'Go', value: 'go' },
24+
{ label: 'Hex', value: 'hex' },
25+
{ label: 'Maven', value: 'maven' },
26+
{ label: 'npm', value: 'npm' },
27+
{ label: 'NuGet', value: 'nuget' },
28+
{ label: 'Opam', value: 'opam' },
29+
{ label: 'OSS-Fuzz', value: 'oss-fuzz' },
30+
{ label: 'Packagist', value: 'packagist' },
31+
{ label: 'Pub', value: 'pub' },
32+
{ label: 'Red-Hat', value: 'red-hat' },
33+
{ label: 'Rubygems', value: 'rubygems' },
34+
]
35+
1536
export default function PurlPageComponent({ purl }: { purl?: string }) {
37+
const router = useRouter()
1638
const purlString = typeof purl === 'string' ? purl : ''
1739
const decodedPurl = purlString ? decodeURIComponent(purlString) : ''
1840
const packageName = decodedPurl ? extractPackageName(decodedPurl) : ''
1941

42+
const [searchOpen, setSearchOpen] = useState(false)
43+
const [ecosystem, setEcosystem] = useState('npm')
44+
const [searchName, setSearchName] = useState('')
45+
const [version, setVersion] = useState('')
46+
const [searchError, setSearchError] = useState<string | null>(null)
47+
const [isSearching, setIsSearching] = useState(false)
48+
49+
useEffect(() => {
50+
setSearchOpen(false)
51+
setSearchName('')
52+
setVersion('')
53+
setSearchError(null)
54+
setIsSearching(false)
55+
}, [purl])
56+
57+
const handleSearchSubmit = async () => {
58+
setSearchError(null)
59+
if (!searchName.trim() || !version.trim()) {
60+
setSearchError('Please fill in both fields.')
61+
return
62+
}
63+
const versionPart = version.trim() ? `@${version.trim()}` : ''
64+
const newPurl = `pkg:${ecosystem}/${searchName.trim()}${versionPart}`
65+
setIsSearching(true)
66+
try {
67+
const response = await fetch(
68+
'https://api.main.devguard.org/api/v1/vulndb/purl-inspect/' +
69+
encodeURIComponent(newPurl),
70+
)
71+
if (!response.ok) {
72+
setSearchError('Package not found. Please check your input.')
73+
return
74+
}
75+
const data = await response.json()
76+
if (!data?.component) {
77+
setSearchError('Package not found. Please check your input.')
78+
return
79+
}
80+
router.push('/package-inspector/' + encodeURIComponent(newPurl))
81+
} catch {
82+
setSearchError('Could not reach the API. Please try again.')
83+
} finally {
84+
setIsSearching(false)
85+
}
86+
}
87+
2088
const url = decodedPurl
2189
? 'https://api.main.devguard.org/api/v1/vulndb/purl-inspect/' +
2290
decodedPurl
@@ -100,10 +168,89 @@ export default function PurlPageComponent({ purl }: { purl?: string }) {
100168
</Head>
101169

102170
{/* Header */}
103-
<div className="mb-6">
171+
<div className="mb-6 flex items-end justify-between">
104172
<p className="mb-1 text-sm font-medium uppercase tracking-wider text-gray-500">
105173
Package Inspector
106174
</p>
175+
<div className="relative">
176+
{!searchOpen ? (
177+
<button
178+
type="button"
179+
onClick={() => setSearchOpen(true)}
180+
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800/50 px-3 py-1.5 text-xs text-gray-400 transition-colors hover:border-gray-600 hover:text-gray-300"
181+
>
182+
<Search className="h-3.5 w-3.5" />
183+
Search again
184+
</button>
185+
) : (
186+
<form
187+
onSubmit={(e) => {
188+
e.preventDefault()
189+
handleSearchSubmit()
190+
}}
191+
className="flex flex-col items-end gap-2"
192+
>
193+
<div
194+
className={cn(
195+
'flex items-center rounded-xl border border-input bg-transparent transition-colors focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50',
196+
searchError ? 'border-red-500 focus-within:border-red-500 focus-within:ring-red-500/50' : '',
197+
)}
198+
>
199+
<div className="relative flex items-center">
200+
<select
201+
value={ecosystem}
202+
onChange={(e) => setEcosystem(e.target.value)}
203+
className="h-9 appearance-none bg-transparent pl-2.5 pr-6 text-xs outline-none rounded-l-xl cursor-pointer"
204+
disabled={isSearching}
205+
>
206+
{ECOSYSTEMS.map((eco) => (
207+
<option key={eco.value} value={eco.value}>
208+
{eco.label}
209+
</option>
210+
))}
211+
</select>
212+
<ChevronDown className="pointer-events-none absolute right-1 h-3 w-3 text-muted-foreground" />
213+
</div>
214+
<div className="h-6 w-px bg-input" />
215+
<input
216+
type="text"
217+
value={searchName}
218+
onChange={(e) => setSearchName(e.target.value)}
219+
placeholder="name"
220+
className="h-9 w-24 min-w-0 bg-transparent px-2 text-xs outline-none placeholder:text-muted-foreground"
221+
disabled={isSearching}
222+
autoFocus
223+
/>
224+
<div className="h-6 w-px bg-input" />
225+
<input
226+
type="text"
227+
value={version}
228+
onChange={(e) => {
229+
setVersion(e.target.value)
230+
setSearchError(null)
231+
}}
232+
placeholder="version"
233+
className="h-9 w-20 bg-transparent px-2 text-xs outline-none placeholder:text-muted-foreground"
234+
disabled={isSearching}
235+
/>
236+
<button
237+
type="button"
238+
onClick={() => {
239+
setSearchOpen(false)
240+
setSearchError(null)
241+
}}
242+
className="mr-1.5 flex items-center text-gray-500 hover:text-gray-300"
243+
>
244+
<X className="h-3.5 w-3.5" />
245+
</button>
246+
</div>
247+
<button type="submit" className="hidden" />
248+
{searchError && (
249+
<span className="text-xs text-red-400">{searchError}</span>
250+
)}
251+
</form>
252+
)}
253+
</div>
107254
</div>
108255

109256
<PackageHeroCard

src/components/vulnerability-database/cve-detail.tsx

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { useState } from 'react'
1+
import { useState, useEffect } from 'react'
22
import { affectedComponent } from '@/types/api'
33
import { ChartContainer } from '@/components/ui/chart'
44
import { Label, Pie, PieChart } from 'recharts'
55
import { Skeleton } from '@/components/ui/skeleton'
66
import { Button } from '@/components/ui/button'
77
import Link from 'next/link'
8-
import { ExternalLink, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'
8+
import { useRouter } from 'next/router'
9+
import { ExternalLink, ChevronLeft, ChevronRight, ChevronDown, Search, X } from 'lucide-react'
910
import { fetcher } from '@/lib/fetcher'
1011
import useSWR from 'swr'
1112
import { Container } from '../ui/container'
@@ -60,13 +61,27 @@ function formatVersionRange(ac: affectedComponent): string | null {
6061
const ITEMS_PER_PAGE = 3
6162

6263
export default function CVEDetailComponent({ cveId }: { cveId?: string }) {
64+
const router = useRouter()
6365
const { data, error, isLoading } = useSWR<CVEData>(
6466
cveId ? `https://api.main.devguard.org/api/v1/vulndb/${cveId}` : null,
6567
fetcher,
6668
)
6769

6870
const [page, setPage] = useState(0)
6971
const [descExpanded, setDescExpanded] = useState(false)
72+
const [searchOpen, setSearchOpen] = useState(false)
73+
const [searchTerm, setSearchTerm] = useState('')
74+
75+
useEffect(() => {
76+
setSearchOpen(false)
77+
setSearchTerm('')
78+
}, [cveId])
79+
80+
const handleCveSearch = () => {
81+
const trimmed = searchTerm.trim()
82+
if (!trimmed) return
83+
router.push(`/vulnerability-database?query=${encodeURIComponent(trimmed)}&page=1`)
84+
}
7085

7186
const options: Intl.DateTimeFormatOptions = {
7287
year: 'numeric',
@@ -156,7 +171,7 @@ export default function CVEDetailComponent({ cveId }: { cveId?: string }) {
156171
},
157172
]
158173

159-
const allComponents = data.affectedComponents.filter((ac) => {
174+
const allComponents = (data.affectedComponents ?? []).filter((ac) => {
160175
return (
161176
ac.semverStart ||
162177
ac.semverEnd ||
@@ -187,10 +202,50 @@ export default function CVEDetailComponent({ cveId }: { cveId?: string }) {
187202
<div className="pointer-events-none fixed inset-y-0 right-0 z-50 hidden w-8 border-l border-l-[var(--grid-line-color)] bg-[repeating-linear-gradient(315deg,var(--grid-line-color)_0,var(--grid-line-color)_1px,transparent_0,transparent_50%)] [background-size:10px_10px] bg-fixed opacity-80 sm:block" />
188203
<div className="mx-auto max-w-6xl p-8">
189204
{/* Header */}
190-
<div className="mb-6">
205+
<div className="mb-6 flex items-start justify-between">
191206
<p className="mb-1 text-xs font-medium uppercase tracking-widest text-gray-500">
192207
CVE Detail
193208
</p>
209+
<div className="relative">
210+
{!searchOpen ? (
211+
<button
212+
type="button"
213+
onClick={() => setSearchOpen(true)}
214+
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800/50 px-3 py-1.5 text-xs text-gray-400 transition-colors hover:border-gray-600 hover:text-gray-300"
215+
>
216+
<Search className="h-3.5 w-3.5" />
217+
Search again
218+
</button>
219+
) : (
220+
<form
221+
onSubmit={(e) => {
222+
e.preventDefault()
223+
handleCveSearch()
224+
}}
225+
className="flex items-center gap-2"
226+
>
227+
<div className="flex items-center rounded-xl border border-input bg-transparent transition-colors focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50">
228+
<Search className="ml-2.5 h-3.5 w-3.5 text-muted-foreground" />
229+
<input
230+
type="text"
231+
value={searchTerm}
232+
onChange={(e) => setSearchTerm(e.target.value)}
233+
placeholder="CVE-2024-..."
234+
className="h-9 w-40 min-w-0 bg-transparent px-2 text-xs outline-none placeholder:text-muted-foreground"
235+
autoFocus
236+
/>
237+
<button
238+
type="button"
239+
onClick={() => setSearchOpen(false)}
240+
className="mr-1.5 flex items-center text-gray-500 hover:text-gray-300"
241+
>
242+
<X className="h-3.5 w-3.5" />
243+
</button>
244+
</div>
245+
<button type="submit" className="hidden" />
246+
</form>
247+
)}
248+
</div>
194249
</div>
195250

196251
{/* Hero section */}

0 commit comments

Comments
 (0)