Skip to content

Commit a830f28

Browse files
committed
feat: add graph visualization for requirement files
1 parent b76afa9 commit a830f28

File tree

7 files changed

+109
-24
lines changed

7 files changed

+109
-24
lines changed

components/feature/depex/Operations.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ export function Operations({
225225
<SSCOperationsForm
226226
onExecute={handleSSCOperation}
227227
disabled={showLoading}
228-
229228
/>
230229
</TabsContent>
231230

components/feature/depex/PackageGraphView.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export default function PackageGraphView({ open, onOpenChange, packageName, purl
172172
if (type === 'CargoPackage') return '#f74c00' // Rust orange
173173
if (type === 'RubyGemsPackage') return '#701516' // Ruby dark red/burgundy
174174
if (type === 'MavenPackage') return '#f89820' // Maven orange/yellow
175+
if (type === 'RequirementFile') return '#10b981' // Green for requirement files
175176
if (type.includes('Package')) return '#1e3a8a' // Default deep blue
176177
return '#3b82f6' // Version: lighter blue
177178
}
@@ -184,6 +185,7 @@ export default function PackageGraphView({ open, onOpenChange, packageName, purl
184185
if (type === 'CargoPackage') return 'Cargo'
185186
if (type === 'RubyGemsPackage') return 'RubyGems'
186187
if (type === 'MavenPackage') return 'Maven'
188+
if (type === 'RequirementFile') return 'File'
187189
return null
188190
}
189191

@@ -725,14 +727,14 @@ export default function PackageGraphView({ open, onOpenChange, packageName, purl
725727

726728
<div className="space-y-2 pt-2 border-t">
727729
<div className="font-medium text-xs text-muted-foreground">Node Types</div>
728-
<div className="flex items-center gap-2">
729-
<div className="w-4 h-4 rounded-full border-2 border-[#3776ab] bg-[#e6f2ff]"></div>
730-
<span>Package</span>
731-
</div>
732730
<div className="flex items-center gap-2">
733731
<div className="w-4 h-4 rounded-full border-2 border-[#3b82f6] bg-[#dbeafe]"></div>
734732
<span>Version</span>
735733
</div>
734+
<div className="flex items-center gap-2">
735+
<div className="w-4 h-4 rounded-full border-2 border-[#10b981] bg-[#d1fae5]"></div>
736+
<span>Requirement File</span>
737+
</div>
736738
<div className="flex items-center gap-2">
737739
<div className="relative w-4 h-4 rounded-full border-2 border-[#3b82f6] bg-[#dbeafe]">
738740
<div className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-red-600 border border-white"></div>
@@ -902,12 +904,21 @@ export default function PackageGraphView({ open, onOpenChange, packageName, purl
902904
</div>
903905

904906
<div className="space-y-2 text-sm">
905-
<div>
906-
<div className="text-xs text-muted-foreground">purl</div>
907-
<div className="text-xs break-words bg-muted p-1 rounded font-mono">
908-
{selected.props?.purl || 'N/A'}
907+
{selected.type !== 'RequirementFile' && (
908+
<div>
909+
<div className="text-xs text-muted-foreground">purl</div>
910+
<div className="text-xs break-words bg-muted p-1 rounded font-mono">
911+
{selected.props?.purl || 'N/A'}
912+
</div>
909913
</div>
910-
</div>
914+
)}
915+
916+
{selected.type === 'RequirementFile' && (
917+
<div>
918+
<div className="text-xs text-muted-foreground">file name</div>
919+
<div className="text-sm font-medium">{selected.label}</div>
920+
</div>
921+
)}
911922

912923
{selected.type === 'Version' && (
913924
<>

components/feature/depex/RepositoryCard.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

33
import { Badge, Button } from '@/components/ui'
4-
import { Package, CheckCircle, XCircle, Settings } from 'lucide-react'
4+
import { Package, CheckCircle, XCircle, Settings, Network } from 'lucide-react'
55
import dynamic from 'next/dynamic'
66
import { useState } from 'react'
77
import type { Repository, RequirementFile } from '@/types'
88
import { OperationsModal } from './OperationsModal'
99
import { VEXGenButton } from '@/components/feature/vexgen'
10+
import PackageGraphView from './PackageGraphView'
1011

1112
const GitHubIcon = dynamic(
1213
() => import('react-icons/si').then(mod => ({ default: mod.SiGithub })),
@@ -23,12 +24,19 @@ interface RepositoryCardProps {
2324
export default function RepositoryCard({ repository }: RepositoryCardProps) {
2425
const [selectedFile, setSelectedFile] = useState<RequirementFile | null>(null)
2526
const [isModalOpen, setIsModalOpen] = useState(false)
27+
const [showGraph, setShowGraph] = useState(false)
28+
const [graphFile, setGraphFile] = useState<RequirementFile | null>(null)
2629

2730
const handleOperationsClick = (file: RequirementFile) => {
2831
setSelectedFile(file)
2932
setIsModalOpen(true)
3033
}
3134

35+
const handleViewGraph = (file: RequirementFile) => {
36+
setGraphFile(file)
37+
setShowGraph(true)
38+
}
39+
3240
return (
3341
<div className="border rounded-lg p-3 sm:p-4 bg-card hover:bg-accent/50 transition-colors">
3442
<div className="flex items-start gap-3 mb-3">
@@ -92,6 +100,18 @@ export default function RepositoryCard({ repository }: RepositoryCardProps) {
92100
<Badge variant="outline" className="text-xs">
93101
{file.manager}
94102
</Badge>
103+
<Button
104+
size="sm"
105+
variant="outline"
106+
onClick={() => handleViewGraph(file)}
107+
className="h-6 w-6 p-0 sm:h-8 sm:w-auto sm:px-2"
108+
title="View Graph"
109+
>
110+
<Network className="h-3 w-3 sm:mr-1" />
111+
<span className="hidden sm:inline text-xs">
112+
Graph
113+
</span>
114+
</Button>
95115
<Button
96116
size="sm"
97117
variant="outline"
@@ -131,6 +151,17 @@ export default function RepositoryCard({ repository }: RepositoryCardProps) {
131151
fileManager={selectedFile?.manager || ''}
132152

133153
/>
154+
155+
{/* Package Graph View */}
156+
{graphFile && (
157+
<PackageGraphView
158+
open={showGraph}
159+
onOpenChange={setShowGraph}
160+
packageName={graphFile.name}
161+
purl={graphFile.requirement_file_id}
162+
nodeType="RequirementFile"
163+
/>
164+
)}
134165
</div>
135166
)
136167
}

components/feature/depex/SSCOperationsForm.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import { InfoIcon } from 'lucide-react'
1717
interface SSCOperationsFormProps {
1818
onExecute: (_operation: string, _params: any) => void
1919
disabled?: boolean
20-
}
20+
onViewGraph?: () => void
21+
}
2122

22-
export function SSCOperationsForm({ onExecute, disabled }: SSCOperationsFormProps) {
23+
export function SSCOperationsForm({ onExecute, disabled, onViewGraph }: SSCOperationsFormProps) {
2324
const [selectedOperation, setSelectedOperation] = useState<string>('')
2425
const [params, setParams] = useState<{
2526
maxDepth: number | string
@@ -210,8 +211,13 @@ export function SSCOperationsForm({ onExecute, disabled }: SSCOperationsFormProp
210211
</div>
211212
)}
212213

213-
<div className="flex justify-end pt-4">
214-
<Button onClick={handleExecute} disabled={disabled || !selectedOperation}>
214+
<div className="flex justify-between pt-4">
215+
{onViewGraph && (
216+
<Button variant="outline" onClick={onViewGraph}>
217+
View Graph
218+
</Button>
219+
)}
220+
<Button onClick={handleExecute} disabled={disabled || !selectedOperation} className={!onViewGraph ? 'ml-auto' : ''}>
215221
{disabled
216222
? 'Executing...'
217223
: 'Execute'}

constants/apiEndpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const API_ENDPOINTS = {
2222
GRAPH: {
2323
EXPAND_PACKAGE: '/api/depex/graph/expand/package',
2424
EXPAND_VERSION: '/api/depex/graph/expand/version',
25+
EXPAND_REQ_FILE: '/api/depex/graph/expand/req_file',
2526
},
2627
OPERATION: {
2728
SSC: {

hooks/api/usePackageGraph.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ interface UsePackageGraphProps {
1212

1313
export function usePackageGraph({ packageName, purl, nodeType }: UsePackageGraphProps) {
1414
const [graph, setGraph] = useState<GraphResponse>(() => {
15+
const props: any = {
16+
name: packageName,
17+
}
18+
19+
if (nodeType !== 'RequirementFile') {
20+
props.purl = purl
21+
}
22+
1523
return {
1624
nodes: [{
1725
id: purl,
1826
label: packageName,
1927
type: nodeType,
20-
props: {
21-
name: packageName,
22-
purl: purl
23-
}
28+
props: props
2429
}],
2530
edges: []
2631
}
@@ -46,6 +51,10 @@ export function usePackageGraph({ packageName, purl, nodeType }: UsePackageGraph
4651
response = await depexAPI.graph.expandVersion({
4752
version_purl: node.props?.purl || nodeId
4853
})
54+
} else if (node.type === 'RequirementFile') {
55+
response = await depexAPI.graph.expandReqFile({
56+
requirement_file_id: nodeId
57+
})
4958
} else {
5059
// Find the REQUIRE edge that points to this package to get constraints
5160
const requireEdge = graph.edges.find(
@@ -72,14 +81,29 @@ export function usePackageGraph({ packageName, purl, nodeType }: UsePackageGraph
7281

7382
const currentIds = new Set(current.nodes.map(n => n.id))
7483
const addedNodeIds: string[] = []
84+
85+
// Filter out nodes with null id and process valid nodes
7586
expandData.nodes.forEach((n: GraphNode) => {
87+
// Skip nodes with null or undefined id
88+
if (n.id == null) {
89+
console.warn('Skipping node with null id:', n)
90+
return
91+
}
92+
7693
if (!currentIds.has(n.id)) addedNodeIds.push(n.id)
7794
nodesById[n.id] = { ...nodesById[n.id], ...n }
7895
})
7996

8097
const edgesById: Record<string, GraphEdge> = {}
8198
current.edges.forEach((e: GraphEdge) => (edgesById[e.id] = e))
99+
100+
// Filter out edges with null source or target
82101
expandData.edges.forEach((e: GraphEdge) => {
102+
// Skip edges with null id, source, or target
103+
if (e.id == null || e.source == null || e.target == null) {
104+
console.warn('Skipping edge with null values:', e)
105+
return
106+
}
83107
edgesById[e.id] = e
84108
})
85109

@@ -96,7 +120,7 @@ export function usePackageGraph({ packageName, purl, nodeType }: UsePackageGraph
96120
} finally {
97121
setLoadingNodes((s: Record<string, boolean>) => ({ ...s, [nodeId]: false }))
98122
}
99-
}, [fetched, graph.nodes])
123+
}, [fetched, graph])
100124

101125
const collapseNode = useCallback((nodeId: string) => {
102126
const toRemove: string[] = []
@@ -142,15 +166,21 @@ export function usePackageGraph({ packageName, purl, nodeType }: UsePackageGraph
142166
}, [expansions])
143167

144168
const reload = useCallback(() => {
169+
// Para RequirementFile, no incluir purl en props
170+
const props: any = {
171+
name: packageName,
172+
}
173+
174+
if (nodeType !== 'RequirementFile') {
175+
props.purl = purl
176+
}
177+
145178
setGraph({
146179
nodes: [{
147180
id: purl,
148181
label: packageName,
149182
type: nodeType,
150-
props: {
151-
name: packageName,
152-
purl: purl
153-
}
183+
props: props
154184
}],
155185
edges: []
156186
})

lib/api/apiClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@ export const depexAPI = {
410410
})
411411
return apiClient.get(`${API_ENDPOINTS.DEPEX.GRAPH.EXPAND_VERSION}?${params.toString()}`, { retries: 0 })
412412
},
413+
414+
expandReqFile: (data: { requirement_file_id: string }) => {
415+
const params = new URLSearchParams({
416+
requirement_file_id: data.requirement_file_id,
417+
})
418+
return apiClient.get(`${API_ENDPOINTS.DEPEX.GRAPH.EXPAND_REQ_FILE}?${params.toString()}`, { retries: 0 })
419+
},
413420
},
414421

415422
operations: {

0 commit comments

Comments
 (0)