Optimized package analysis
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
import { Label } from "./components/ui/label";
|
import { Label } from "./components/ui/label";
|
||||||
import { Input } from "./components/ui/input";
|
import { Input } from "./components/ui/input";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react";
|
||||||
import { Edit2, ChevronDown, ChevronUp, AlertCircle, Search, Package, Upload, CheckCircle2, XCircle, AlertTriangle, FileJson, TrendingUp, TrendingDown, Copy, Check, Terminal, X, Save, FolderOpen } from "lucide-react";
|
import { Edit2, ChevronDown, ChevronUp, AlertCircle, Search, Package, Upload, CheckCircle2, XCircle, AlertTriangle, FileJson, TrendingUp, TrendingDown, Copy, Check, Terminal, X, Save, FolderOpen } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PackageSelector } from "./PackageSelector";
|
import { PackageSelector } from "./PackageSelector";
|
||||||
@@ -98,7 +98,7 @@ function satisfiesVersion(installedVersion: string, requiredRange: string): bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PackageResultCard({ result, onVersionChange, nodeVersion }: {
|
const PackageResultCard = memo(function PackageResultCard({ result, onVersionChange, nodeVersion }: {
|
||||||
result: AnalysisResult;
|
result: AnalysisResult;
|
||||||
onVersionChange: (packageName: string, version: string) => void;
|
onVersionChange: (packageName: string, version: string) => void;
|
||||||
nodeVersion: string;
|
nodeVersion: string;
|
||||||
@@ -111,11 +111,24 @@ function PackageResultCard({ result, onVersionChange, nodeVersion }: {
|
|||||||
const hasVersionChange = result.selectedVersion && result.selectedVersion !== result.currentVersion;
|
const hasVersionChange = result.selectedVersion && result.selectedVersion !== result.currentVersion;
|
||||||
const hasCompatibilityIssue = !result.nodeCompatible || (result.peerConflicts && result.peerConflicts.length > 0);
|
const hasCompatibilityIssue = !result.nodeCompatible || (result.peerConflicts && result.peerConflicts.length > 0);
|
||||||
|
|
||||||
const sortedVersions = result.availableVersions ? sortVersions([...result.availableVersions]) : [];
|
const sortedVersions = useMemo(() =>
|
||||||
const filteredAvailableVersions = sortedVersions.filter(v =>
|
result.availableVersions ? sortVersions([...result.availableVersions]) : [],
|
||||||
v.toLowerCase().includes(versionSearchQuery.toLowerCase())
|
[result.availableVersions]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filteredAvailableVersions = useMemo(() =>
|
||||||
|
sortedVersions.filter(v =>
|
||||||
|
v.toLowerCase().includes(versionSearchQuery.toLowerCase())
|
||||||
|
),
|
||||||
|
[sortedVersions, versionSearchQuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVersionChange = useCallback((version: string) => {
|
||||||
|
onVersionChange(result.name, version);
|
||||||
|
setVersionDropdownOpen(false);
|
||||||
|
setVersionSearchQuery("");
|
||||||
|
}, [result.name, onVersionChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!versionDropdownOpen) return;
|
if (!versionDropdownOpen) return;
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -233,11 +246,7 @@ function PackageResultCard({ result, onVersionChange, nodeVersion }: {
|
|||||||
filteredAvailableVersions.map((version) => (
|
filteredAvailableVersions.map((version) => (
|
||||||
<button
|
<button
|
||||||
key={version}
|
key={version}
|
||||||
onClick={() => {
|
onClick={() => handleVersionChange(version)}
|
||||||
onVersionChange(result.name, version);
|
|
||||||
setVersionDropdownOpen(false);
|
|
||||||
setVersionSearchQuery('');
|
|
||||||
}}
|
|
||||||
className={`w-full text-left px-3 py-2 rounded-md transition-colors ${
|
className={`w-full text-left px-3 py-2 rounded-md transition-colors ${
|
||||||
version === activeVersion
|
version === activeVersion
|
||||||
? 'bg-accent font-medium border border-border'
|
? 'bg-accent font-medium border border-border'
|
||||||
@@ -419,7 +428,7 @@ function PackageResultCard({ result, onVersionChange, nodeVersion }: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export function PackageDetails() {
|
export function PackageDetails() {
|
||||||
const [packages, setPackages] = useState<PackageData[]>([]);
|
const [packages, setPackages] = useState<PackageData[]>([]);
|
||||||
@@ -480,7 +489,7 @@ export function PackageDetails() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVersionChange = (packageName: string, version: string) => {
|
const handleVersionChange = useCallback((packageName: string, version: string) => {
|
||||||
const newSelections = new Map(versionSelections);
|
const newSelections = new Map(versionSelections);
|
||||||
newSelections.set(packageName, version);
|
newSelections.set(packageName, version);
|
||||||
setVersionSelections(newSelections);
|
setVersionSelections(newSelections);
|
||||||
@@ -505,7 +514,7 @@ export function PackageDetails() {
|
|||||||
} else {
|
} else {
|
||||||
toast.info(`${packageName} → ${version}`);
|
toast.info(`${packageName} → ${version}`);
|
||||||
}
|
}
|
||||||
};
|
}, [versionSelections, analysisResults, nodeVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (analysisResults.length > 0) {
|
if (analysisResults.length > 0) {
|
||||||
@@ -995,44 +1004,69 @@ export function PackageDetails() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPkgs = packages.filter(p => selectedPackages.has(p.name));
|
const selectedPkgs = useMemo(() =>
|
||||||
const filteredVersions = nodeVersions.filter(v => {
|
packages.filter(p => selectedPackages.has(p.name)),
|
||||||
const searchLower = nodeVersionSearch.toLowerCase();
|
[packages, selectedPackages]
|
||||||
const matchesVersion = v.version.toLowerCase().includes(searchLower);
|
);
|
||||||
const matchesLtsName = typeof v.lts === 'string' && v.lts.toLowerCase().includes(searchLower);
|
|
||||||
const matchesLtsKeyword = v.lts && searchLower.includes('lts');
|
|
||||||
return matchesVersion || matchesLtsName || matchesLtsKeyword;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedVersionInfo = nodeVersions.find(v => v.version.replace('v', '') === nodeVersion);
|
const filteredVersions = useMemo(() =>
|
||||||
|
nodeVersions.filter(v => {
|
||||||
|
const searchLower = nodeVersionSearch.toLowerCase();
|
||||||
|
const matchesVersion = v.version.toLowerCase().includes(searchLower);
|
||||||
|
const matchesLtsName = typeof v.lts === 'string' && v.lts.toLowerCase().includes(searchLower);
|
||||||
|
const matchesLtsKeyword = v.lts && searchLower.includes('lts');
|
||||||
|
return matchesVersion || matchesLtsName || matchesLtsKeyword;
|
||||||
|
}),
|
||||||
|
[nodeVersions, nodeVersionSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedVersionInfo = useMemo(() =>
|
||||||
|
nodeVersions.find(v => v.version.replace('v', '') === nodeVersion),
|
||||||
|
[nodeVersions, nodeVersion]
|
||||||
|
);
|
||||||
|
|
||||||
// Count issues
|
// Count issues
|
||||||
const compatibilityIssues = analysisResults.filter(r => !r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0)).length;
|
const compatibilityIssues = useMemo(() =>
|
||||||
const modifiedPackages = analysisResults.filter(r => r.selectedVersion && r.selectedVersion !== r.currentVersion).length;
|
analysisResults.filter(r => !r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0)).length,
|
||||||
|
[analysisResults]
|
||||||
|
);
|
||||||
|
|
||||||
|
const modifiedPackages = useMemo(() =>
|
||||||
|
analysisResults.filter(r => r.selectedVersion && r.selectedVersion !== r.currentVersion).length,
|
||||||
|
[analysisResults]
|
||||||
|
);
|
||||||
|
|
||||||
// Get packages with compatibility issues among modified packages
|
// Get packages with compatibility issues among modified packages
|
||||||
const modifiedWithIssues = analysisResults.filter(r =>
|
const modifiedWithIssues = useMemo(() =>
|
||||||
r.selectedVersion &&
|
analysisResults.filter(r =>
|
||||||
r.selectedVersion !== r.currentVersion &&
|
r.selectedVersion &&
|
||||||
(!r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0))
|
r.selectedVersion !== r.currentVersion &&
|
||||||
|
(!r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0))
|
||||||
|
),
|
||||||
|
[analysisResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get unmodified packages with issues
|
// Get unmodified packages with issues
|
||||||
const unmodifiedWithIssues = analysisResults.filter(r =>
|
const unmodifiedWithIssues = useMemo(() =>
|
||||||
(!r.selectedVersion || r.selectedVersion === r.currentVersion) &&
|
analysisResults.filter(r =>
|
||||||
(!r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0))
|
(!r.selectedVersion || r.selectedVersion === r.currentVersion) &&
|
||||||
|
(!r.nodeCompatible || (r.peerConflicts && r.peerConflicts.length > 0))
|
||||||
|
),
|
||||||
|
[analysisResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate npm install command
|
// Generate npm install command
|
||||||
const generateUpgradeCommand = () => {
|
const upgradeCommand = useMemo(() => {
|
||||||
const modifiedPkgs = analysisResults.filter(r => r.selectedVersion && r.selectedVersion !== r.currentVersion);
|
const modifiedPkgs = analysisResults.filter(r => r.selectedVersion && r.selectedVersion !== r.currentVersion);
|
||||||
if (modifiedPkgs.length === 0) return '';
|
if (modifiedPkgs.length === 0) return '';
|
||||||
|
|
||||||
const packages = modifiedPkgs.map(pkg => `${pkg.name}@${pkg.selectedVersion}`).join(' ');
|
const packages = modifiedPkgs.map(pkg => `${pkg.name}@${pkg.selectedVersion}`).join(' ');
|
||||||
return `npm install ${packages}`;
|
return `npm install ${packages}`;
|
||||||
};
|
}, [analysisResults]);
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const generateUpgradeCommand = () => upgradeCommand;
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopiedCommand(true);
|
setCopiedCommand(true);
|
||||||
@@ -1041,7 +1075,7 @@ export function PackageDetails() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to copy to clipboard');
|
toast.error('Failed to copy to clipboard');
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background">
|
<div className="bg-background">
|
||||||
|
|||||||
Reference in New Issue
Block a user