- Fix sidebar layout: use flexbox instead of margin-left approach - Update sidebar to use responsive display (hidden on mobile, static on desktop) - Fix mobile overlay z-index and positioning issues - Add proper flex container structure to all pages - Add new dashboard pages: health, metrics, system, env, code-quality, settings/cosmos - Add comprehensive API client and type definitions - Add error boundary and log viewer components - Add test infrastructure with Vitest and Playwright - Add Docker configuration and deployment scripts Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
344 lines
15 KiB
TypeScript
344 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Play, CheckCircle, XCircle, AlertTriangle, FileText, Clock, Code2, Bug, Loader2 } from 'lucide-react';
|
|
import { runCodeQualityCheck, type CodeQualityReport, type CodeQualityIssue } from '@/lib/api';
|
|
import { SidebarNav } from '@/components/sidebar-nav';
|
|
|
|
export default function CodeQualityPage() {
|
|
const [projectPath, setProjectPath] = useState('');
|
|
const [projectId, setProjectId] = useState('');
|
|
const [selectedChecks, setSelectedChecks] = useState<Array<'typescript' | 'eslint' | 'build' | 'test'>>([
|
|
'typescript',
|
|
'eslint',
|
|
'build',
|
|
'test',
|
|
]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [report, setReport] = useState<CodeQualityReport | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleRunCheck = async () => {
|
|
if (!projectPath || !projectId) {
|
|
setError('Please provide project ID and path');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
setReport(null);
|
|
|
|
try {
|
|
const result = await runCodeQualityCheck({
|
|
projectId,
|
|
projectPath,
|
|
checks: selectedChecks,
|
|
});
|
|
setReport(result);
|
|
} catch (err) {
|
|
setError('Failed to run code quality check');
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleCheck = (check: 'typescript' | 'eslint' | 'build' | 'test') => {
|
|
setSelectedChecks(prev =>
|
|
prev.includes(check) ? prev.filter(c => c !== check) : [...prev, check]
|
|
);
|
|
};
|
|
|
|
const getIssueIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'error':
|
|
return <XCircle className="w-4 h-4 text-red-600" />;
|
|
case 'warning':
|
|
return <AlertTriangle className="w-4 h-4 text-yellow-600" />;
|
|
case 'info':
|
|
return <FileText className="w-4 h-4 text-blue-600" />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getCategoryIcon = (category: string) => {
|
|
switch (category) {
|
|
case 'typescript':
|
|
return <Code2 className="w-4 h-4 text-blue-600" />;
|
|
case 'eslint':
|
|
return <FileText className="w-4 h-4 text-purple-600" />;
|
|
case 'build':
|
|
return <Play className="w-4 h-4 text-green-600" />;
|
|
case 'test':
|
|
return <Bug className="w-4 h-4 text-orange-600" />;
|
|
default:
|
|
return <FileText className="w-4 h-4 text-gray-600" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-gray-50">
|
|
<SidebarNav />
|
|
|
|
<main className="flex-1 min-w-0 overflow-y-auto">
|
|
<div className="p-8 max-md:p-4">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Code Quality Analysis</h1>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
|
Run TypeScript, ESLint, build, and test checks on your projects
|
|
</p>
|
|
</div>
|
|
|
|
{/* Configuration */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Configuration</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Project ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={projectId}
|
|
onChange={(e) => setProjectId(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="e.g., trading-service"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Project Path
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={projectPath}
|
|
onChange={(e) => setProjectPath(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
placeholder="e.g., /opt/bytelyst/learning_ai_invt_trdg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Checks to Run
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{(['typescript', 'eslint', 'build', 'test'] as const).map((check) => (
|
|
<button
|
|
key={check}
|
|
onClick={() => toggleCheck(check)}
|
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
selectedChecks.includes(check)
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
}`}
|
|
>
|
|
{check.charAt(0).toUpperCase() + check.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleRunCheck}
|
|
disabled={loading || selectedChecks.length === 0}
|
|
className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Running Checks...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="w-4 h-4" />
|
|
Run Analysis
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded-lg mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{report && (
|
|
<div className="space-y-6">
|
|
{/* Summary */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Summary</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
|
{report.summary.totalIssues}
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Total Issues</div>
|
|
</div>
|
|
<div className="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
|
|
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
|
|
{report.summary.errors}
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Errors</div>
|
|
</div>
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
|
|
<div className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
{report.summary.warnings}
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Warnings</div>
|
|
</div>
|
|
<div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
|
|
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
|
|
{report.categories.test.passed}
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">Tests Passed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Code2 className="w-5 h-5 text-blue-600" />
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">TypeScript</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Errors:</span>
|
|
<span className="font-medium text-red-600">{report.categories.typescript.errors}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Warnings:</span>
|
|
<span className="font-medium text-yellow-600">{report.categories.typescript.warnings}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Duration:</span>
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{report.categories.typescript.duration}ms
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<FileText className="w-5 h-5 text-purple-600" />
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">ESLint</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Errors:</span>
|
|
<span className="font-medium text-red-600">{report.categories.eslint.errors}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Warnings:</span>
|
|
<span className="font-medium text-yellow-600">{report.categories.eslint.warnings}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Duration:</span>
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{report.categories.eslint.duration}ms
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Play className="w-5 h-5 text-green-600" />
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Build</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Status:</span>
|
|
<span className={`font-medium ${report.categories.build.success ? 'text-green-600' : 'text-red-600'}`}>
|
|
{report.categories.build.success ? 'Success' : 'Failed'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Errors:</span>
|
|
<span className="font-medium text-red-600">{report.categories.build.errors}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Duration:</span>
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{report.categories.build.duration}ms
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Bug className="w-5 h-5 text-orange-600" />
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Tests</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Status:</span>
|
|
<span className={`font-medium ${report.categories.test.success ? 'text-green-600' : 'text-red-600'}`}>
|
|
{report.categories.test.success ? 'Success' : 'Failed'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Passed:</span>
|
|
<span className="font-medium text-green-600">{report.categories.test.passed}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600 dark:text-gray-400">Failed:</span>
|
|
<span className="font-medium text-red-600">{report.categories.test.failed}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Issues List */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Issues</h2>
|
|
{report.issues.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<CheckCircle className="w-12 h-12 mx-auto mb-3 text-green-600" />
|
|
<p>No issues found!</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{report.issues.map((issue) => (
|
|
<div
|
|
key={issue.id}
|
|
className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
|
|
>
|
|
<div className="mt-0.5">{getIssueIcon(issue.type)}</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-medium text-gray-900 dark:text-white">{issue.file}</span>
|
|
{issue.line && (
|
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
:{issue.line}
|
|
{issue.column && `:${issue.column}`}
|
|
</span>
|
|
)}
|
|
<div className="ml-auto">{getCategoryIcon(issue.category)}</div>
|
|
</div>
|
|
<p className="text-sm text-gray-700 dark:text-gray-300">{issue.message}</p>
|
|
{issue.rule && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Rule: {issue.rule}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|