Skip to content

Commit ee30c79

Browse files
authored
Merge pull request #6 from TurkNet/csproj-support
Csproj support
2 parents a386bb2 + c8f20db commit ee30c79

9 files changed

Lines changed: 590 additions & 221 deletions

File tree

apps/web/src/api/client.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ export function addVersion(libraryId, payload) {
4242
});
4343
}
4444

45-
export async function scanRepository(url) {
46-
const res = await fetch(`${API_BASE}/libraries/repositories/scan`, {
45+
export async function cloneRepository(url) {
46+
const res = await fetch(`${API_BASE}/libraries/repositories/clone`, {
4747
method: 'POST',
4848
headers: { 'Content-Type': 'application/json' },
4949
body: JSON.stringify({ url })
@@ -55,6 +55,21 @@ export async function scanRepository(url) {
5555
return res.json();
5656
}
5757

58+
export async function listRepositoryPackages({ root, url } = {}) {
59+
const body = JSON.stringify(root ? { root } : { url });
60+
const res = await fetch(`${API_BASE}/libraries/repositories/list-packages`, {
61+
method: 'POST',
62+
headers: { 'Content-Type': 'application/json' },
63+
body,
64+
});
65+
if (!res.ok) {
66+
const message = await res.text();
67+
throw new Error(message || `Request failed with status ${res.status}`);
68+
}
69+
return res.json();
70+
}
71+
72+
5873
export async function analyzeFileUpload(file) {
5974
const form = new FormData();
6075
form.append('file', file);

apps/web/src/components/ImportModal.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { useState, useEffect, useRef } from 'react';
22
import { createLibrary, searchLibraries } from '../api/client.js';
33

4+
5+
//  TODO: RiskScore Gauge component'ı ortak bir yere taşı.
6+
47
const RiskGauge = ({ score, level }) => {
58
if (score === undefined || score === null || Number.isNaN(score)) return null;
69
const clamped = Math.min(100, Math.max(0, Number(score)));

apps/web/src/components/RepoModal.jsx

Lines changed: 183 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useRef, useState } from 'react';
2-
import { scanRepository, searchLibraries, createLibrary } from '../api/client.js';
2+
import { cloneRepository, listRepositoryPackages, searchLibraries, createLibrary } from '../api/client.js';
33

44
const RiskBar = ({ score, explanation }) => {
55
if (score === undefined || score === null || Number.isNaN(score)) return null;
@@ -22,6 +22,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
2222
const [error, setError] = useState(null);
2323
const [loading, setLoading] = useState(false);
2424
const [files, setFiles] = useState([]);
25+
const [statusMessage, setStatusMessage] = useState('');
2526
const [depJobs, setDepJobs] = useState([]);
2627
const [processing, setProcessing] = useState(false);
2728
const inputRef = useRef(null);
@@ -38,6 +39,9 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
3839
return '(N/A)';
3940
};
4041

42+
const totalJobs = depJobs.length;
43+
const processedJobs = depJobs.filter(j => j.status !== 'pending').length;
44+
4145
const resetState = () => {
4246
setRepoUrl('');
4347
setError(null);
@@ -74,6 +78,13 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
7478
return cleaned || null;
7579
}, []);
7680

81+
const cancelledRef = React.useRef(false);
82+
useEffect(() => {
83+
return () => {
84+
cancelledRef.current = true;
85+
};
86+
}, []);
87+
7788
const computeRisk = useCallback((match = {}) => {
7889
const summaries = Array.isArray(match.licenseSummary ?? match.license_summary)
7990
? (match.licenseSummary ?? match.license_summary)
@@ -123,9 +134,18 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
123134
setError(null);
124135
setFiles([]);
125136
setDepJobs([]);
126-
const res = await scanRepository(repoUrl);
127-
const scannedFiles = res.files ?? [];
137+
138+
setStatusMessage('Cloning repository...');
139+
const cloneRes = await cloneRepository(repoUrl);
140+
setStatusMessage('Cloning repository completed.');
141+
const root = cloneRes.root;
142+
143+
setStatusMessage('Listing dependency files...');
144+
const listRes = await listRepositoryPackages({ root });
145+
const scannedFiles = listRes.files ?? cloneRes.files ?? [];
128146
setFiles(scannedFiles);
147+
148+
setStatusMessage('Scanning dependencies...');
129149
const jobs = [];
130150
scannedFiles.forEach((file, fIdx) => {
131151
const deps = Array.isArray(file?.report?.dependencies) ? file.report.dependencies : [];
@@ -145,24 +165,154 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
145165
});
146166
});
147167
setDepJobs(jobs);
168+
// Process jobs sequentially: check local DB, fallback to MCP, persist if needed
169+
setProcessing(true);
170+
const updateJob = (id, patch) =>
171+
setDepJobs(prev => prev.map(j => (j.id === id ? { ...j, ...patch } : j)));
172+
173+
for (const next of jobs) {
174+
if (cancelledRef.current) break;
175+
const q = next.version ? `${next.name} ${next.version}` : next.name;
176+
try {
177+
updateJob(next.id, { status: 'searching', message: null });
178+
const res = await searchLibraries(q);
179+
let match = null;
180+
let existing = false;
181+
182+
if (res?.source === 'mongo' && Array.isArray(res?.results) && res.results.length > 0) {
183+
existing = true;
184+
const lib = res.results[0];
185+
const v = lib.versions?.[0];
186+
match = {
187+
name: lib.name,
188+
version: v?.version,
189+
ecosystem: lib.ecosystem,
190+
description: lib.description,
191+
repository: lib.repository_url,
192+
license: v?.license_name,
193+
license_url: v?.license_url,
194+
licenseSummary: v?.license_summary ?? [],
195+
evidence: v?.evidence ?? [],
196+
confidence: v?.confidence,
197+
risk_level: v?.risk_level,
198+
risk_score: v?.risk_score,
199+
risk_score_explanation: v?.risk_score_explanation
200+
};
201+
} else if (res?.source === 'mcp' && Array.isArray(res?.results) && res.results.length > 0) {
202+
const lib = res.results[0];
203+
const v = lib.versions?.[0];
204+
match = {
205+
name: lib.name,
206+
version: v?.version,
207+
ecosystem: lib.ecosystem,
208+
description: lib.description,
209+
repository: lib.repository_url,
210+
license: v?.license_name,
211+
license_url: v?.license_url,
212+
licenseSummary: v?.license_summary ?? [],
213+
evidence: v?.evidence ?? [],
214+
confidence: v?.confidence,
215+
risk_level: v?.risk_level,
216+
risk_score: v?.risk_score,
217+
risk_score_explanation: v?.risk_score_explanation,
218+
officialSite: lib.officialSite
219+
};
220+
} else if (res?.discovery?.matches?.length) {
221+
match = res.discovery.bestMatch ?? res.discovery.matches[0];
222+
}
223+
224+
if (!match) {
225+
updateJob(next.id, { status: 'error', message: 'Eşleşme bulunamadı' });
226+
continue;
227+
}
228+
229+
const computedRisk = computeRisk(match);
230+
const risk = {
231+
level: match.risk_level ?? computedRisk.level,
232+
score: match.risk_score ?? computedRisk.score,
233+
explanation: match.risk_score_explanation ?? computedRisk.explanation
234+
};
235+
236+
if (existing || res?.source === 'mongo') {
237+
updateJob(next.id, {
238+
status: 'done',
239+
message: 'Zaten kayıtlı',
240+
match,
241+
risk_level: risk.level,
242+
risk_score: risk.score,
243+
risk_score_explanation: risk.explanation
244+
});
245+
continue;
246+
}
247+
248+
updateJob(next.id, { status: 'importing', match, risk_level: risk.level, risk_score: risk.score, risk_score_explanation: risk.explanation });
249+
250+
const payload = {
251+
name: match.name ?? next.name,
252+
ecosystem: match.ecosystem ?? res?.discovery?.query?.ecosystem ?? 'unknown',
253+
description: match.description,
254+
repository_url: match.repository ?? match.officialSite ?? null,
255+
officialSite: match.officialSite ?? match.repository ?? null,
256+
versions: [
257+
{
258+
version: normalizeVersion(match.version ?? next.version) ?? 'unknown',
259+
license_name: match.license ?? null,
260+
license_url: match.license_url ?? null,
261+
notes: match.summary ?? null,
262+
license_summary: Array.isArray(match.licenseSummary)
263+
? match.licenseSummary
264+
.map(item =>
265+
typeof item === 'object' && item !== null
266+
? { summary: item.summary ?? '', emoji: item.emoji ?? null }
267+
: { summary: item, emoji: null }
268+
)
269+
.filter(entry => typeof entry.summary === 'string' && entry.summary.length > 0)
270+
: [],
271+
confidence: match.confidence ?? null,
272+
evidence: Array.isArray(match.evidence) ? match.evidence : [],
273+
risk_level: risk.level,
274+
risk_score: risk.score,
275+
risk_score_explanation: risk.explanation
276+
}
277+
]
278+
};
279+
280+
await createLibrary(payload);
281+
updateJob(next.id, { status: 'done', message: 'Eklendi', match });
282+
if (onImported) onImported();
283+
} catch (err) {
284+
updateJob(next.id, { status: 'error', message: err?.message ?? String(err) });
285+
}
286+
}
287+
288+
setProcessing(false);
148289
} catch (err) {
149-
setError(err.message);
290+
setError(err?.message ?? String(err));
150291
} finally {
151292
setLoading(false);
152293
}
153294
};
154295

155296
useEffect(() => {
297+
let cancelled = false;
298+
156299
const processNext = async () => {
300+
if (cancelled) return;
157301
if (processing) return;
302+
158303
const next = depJobs.find(job => job.status === 'pending');
159304
if (!next) return;
305+
160306
setProcessing(true);
161307
const updateJob = (id, patch) =>
162308
setDepJobs(jobs => jobs.map(j => (j.id === id ? { ...j, ...patch } : j)));
309+
163310
const q = next.version ? `${next.name} ${next.version}` : next.name;
311+
164312
try {
165313
updateJob(next.id, { status: 'searching', message: null });
314+
315+
// 1) Check local DB
166316
const res = await searchLibraries(q);
167317
let match = null;
168318
let existing = false;
@@ -187,6 +337,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
187337
risk_score_explanation: v?.risk_score_explanation
188338
};
189339
} else if (res?.source === 'mcp' && Array.isArray(res?.results) && res.results.length > 0) {
340+
// MCP returned direct results
190341
const lib = res.results[0];
191342
const v = lib.versions?.[0];
192343
match = {
@@ -206,6 +357,7 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
206357
officialSite: lib.officialSite
207358
};
208359
} else if (res?.discovery?.matches?.length) {
360+
// discovery bestMatch or matches
209361
match = res.discovery.bestMatch ?? res.discovery.matches[0];
210362
}
211363

@@ -235,7 +387,9 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
235387
return;
236388
}
237389

390+
// persist MCP/discovery result into local DB
238391
updateJob(next.id, { status: 'importing', match, risk_level: risk.level, risk_score: risk.score, risk_score_explanation: risk.explanation });
392+
239393
const payload = {
240394
name: match.name ?? next.name,
241395
ecosystem: match.ecosystem ?? res?.discovery?.query?.ecosystem ?? 'unknown',
@@ -250,12 +404,12 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
250404
notes: match.summary ?? null,
251405
license_summary: Array.isArray(match.licenseSummary)
252406
? match.licenseSummary
253-
.map(item =>
254-
typeof item === 'object' && item !== null
255-
? { summary: item.summary ?? '', emoji: item.emoji ?? null }
256-
: { summary: item, emoji: null }
257-
)
258-
.filter(entry => typeof entry.summary === 'string' && entry.summary.length > 0)
407+
.map(item =>
408+
typeof item === 'object' && item !== null
409+
? { summary: item.summary ?? '', emoji: item.emoji ?? null }
410+
: { summary: item, emoji: null }
411+
)
412+
.filter(entry => typeof entry.summary === 'string' && entry.summary.length > 0)
259413
: [],
260414
confidence: match.confidence ?? null,
261415
evidence: Array.isArray(match.evidence) ? match.evidence : [],
@@ -270,12 +424,19 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
270424
updateJob(next.id, { status: 'done', message: 'Eklendi', match });
271425
if (onImported) onImported();
272426
} catch (err) {
273-
updateJob(next.id, { status: 'error', message: err.message });
427+
updateJob(next.id, { status: 'error', message: err?.message ?? String(err) });
274428
} finally {
429+
setStatusMessage('Scanning completed.');
275430
setProcessing(false);
276431
}
277432
};
433+
434+
// try to drive processing whenever jobs change
278435
processNext();
436+
437+
return () => {
438+
cancelled = true;
439+
};
279440
}, [depJobs, processing, computeRisk, normalizeVersion, onImported]);
280441

281442
if (!isOpen) return null;
@@ -296,11 +457,11 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
296457
297458
</button>
298459
</div>
299-
<div className="panel" style={{ background: 'transparent', boxShadow: 'none', color: 'white' }}>
460+
<div className="panel" style={{ background: 'transparent', boxShadow: 'none', color: 'white', overflowY: 'visible' }}>
300461
<p>Repo linkini girin, link geçerliyse tarayalım.</p>
301462
<form
302463
className="inline-form"
303-
style={{ marginBottom: '1rem' }}
464+
style={{ marginBottom: '0.5rem' }}
304465
onSubmit={handleSubmit}
305466
>
306467
<input
@@ -311,15 +472,21 @@ export default function RepoLinkModal({ isOpen, onClose, onImported }) {
311472
ref={inputRef}
312473
/>
313474
<div style={{ textAlign: 'right' }}>
314-
<button type="submit" disabled={Boolean(error) || !repoUrl || loading} className="button">
315-
{loading ? 'Taranıyor…' : 'Kontrol'}
475+
<button type="submit" disabled={Boolean(error) || !repoUrl || loading || processing} className="button">
476+
{(loading || processing) ? 'Taranıyor…' : 'Kontrol Et'}
316477
</button>
317478
</div>
318479
</form>
319480
{error && <p className="error">{error}</p>}
481+
{statusMessage && (
482+
<div style={{ marginBottom: '0.5rem', color: '#cbd5f5' }}>
483+
{statusMessage}{(loading || processing) && totalJobs > 0 ? ` (${processedJobs}/${totalJobs})` : ''}
484+
</div>
485+
)}
320486
{files.length > 0 && (
321-
<div style={{ marginTop: '1rem', background: 'rgba(255,255,255,0.05)', padding: '0.75rem', borderRadius: '12px' }}>
487+
<div style={{ marginTop: '1rem', background: 'rgba(255,255,255,0.05)', padding: '0.75rem', borderRadius: '12px', maxHeight: '320px', overflowY: 'auto', WebkitOverflowScrolling: 'touch' }}>
322488
<p style={{ marginTop: 0, marginBottom: '0.5rem' }}>Bulunan dependency dosyaları:</p>
489+
323490
<ul style={{ margin: 0, paddingLeft: 0, color: 'white', listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
324491
{files.map((f, idx) => {
325492
const deps = jobsByFile(f.path);

apps/web/src/styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ body {
136136
display: flex;
137137
justify-content: space-between;
138138
align-items: center;
139-
margin-bottom: 1rem;
139+
margin-bottom: 0.5rem;
140140
}
141141

142142
.modal-header .close {

0 commit comments

Comments
 (0)