re-uploading work
This commit is contained in:
99
web/src/components/packs/PackTestBadge.tsx
Normal file
99
web/src/components/packs/PackTestBadge.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { CheckCircle, XCircle, Clock, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface PackTestBadgeProps {
|
||||
status: string;
|
||||
passed?: number;
|
||||
total?: number;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
showCounts?: boolean;
|
||||
}
|
||||
|
||||
export default function PackTestBadge({
|
||||
status,
|
||||
passed,
|
||||
total,
|
||||
size = 'md',
|
||||
showCounts = true,
|
||||
}: PackTestBadgeProps) {
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'passed':
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
text: 'Passed',
|
||||
bgColor: 'bg-green-50',
|
||||
textColor: 'text-green-700',
|
||||
borderColor: 'border-green-200',
|
||||
iconColor: 'text-green-600',
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
icon: XCircle,
|
||||
text: 'Failed',
|
||||
bgColor: 'bg-red-50',
|
||||
textColor: 'text-red-700',
|
||||
borderColor: 'border-red-200',
|
||||
iconColor: 'text-red-600',
|
||||
};
|
||||
case 'skipped':
|
||||
return {
|
||||
icon: Clock,
|
||||
text: 'Skipped',
|
||||
bgColor: 'bg-gray-50',
|
||||
textColor: 'text-gray-700',
|
||||
borderColor: 'border-gray-200',
|
||||
iconColor: 'text-gray-600',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
text: 'Unknown',
|
||||
bgColor: 'bg-yellow-50',
|
||||
textColor: 'text-yellow-700',
|
||||
borderColor: 'border-yellow-200',
|
||||
iconColor: 'text-yellow-600',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getSizeClasses = () => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return {
|
||||
container: 'px-2 py-1 text-xs',
|
||||
icon: 'w-3 h-3',
|
||||
gap: 'gap-1',
|
||||
};
|
||||
case 'lg':
|
||||
return {
|
||||
container: 'px-4 py-2 text-base',
|
||||
icon: 'w-5 h-5',
|
||||
gap: 'gap-2',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
container: 'px-3 py-1.5 text-sm',
|
||||
icon: 'w-4 h-4',
|
||||
gap: 'gap-1.5',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
const sizeClasses = getSizeClasses();
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center ${sizeClasses.gap} ${sizeClasses.container} ${config.bgColor} ${config.textColor} border ${config.borderColor} rounded-full font-medium`}
|
||||
>
|
||||
<Icon className={`${sizeClasses.icon} ${config.iconColor}`} />
|
||||
<span>{config.text}</span>
|
||||
{showCounts && passed !== undefined && total !== undefined && (
|
||||
<span className="font-semibold">
|
||||
{passed}/{total}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
212
web/src/components/packs/PackTestHistory.tsx
Normal file
212
web/src/components/packs/PackTestHistory.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, Clock } from 'lucide-react';
|
||||
import PackTestBadge from './PackTestBadge';
|
||||
|
||||
interface TestExecution {
|
||||
id: number;
|
||||
pack_id: number;
|
||||
pack_version: string;
|
||||
execution_time: string;
|
||||
trigger_reason: string;
|
||||
total_tests: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
pass_rate: number;
|
||||
duration_ms: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface PackTestHistoryProps {
|
||||
executions: TestExecution[];
|
||||
isLoading?: boolean;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
export default function PackTestHistory({
|
||||
executions,
|
||||
isLoading = false,
|
||||
onLoadMore,
|
||||
hasMore = false,
|
||||
}: PackTestHistoryProps) {
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
const getTriggerBadgeColor = (trigger: string) => {
|
||||
switch (trigger.toLowerCase()) {
|
||||
case 'register':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'manual':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'ci':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'schedule':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatus = (execution: TestExecution): string => {
|
||||
if (execution.status) return execution.status;
|
||||
if (execution.failed > 0) return 'failed';
|
||||
if (execution.passed === execution.total_tests) return 'passed';
|
||||
return 'partial';
|
||||
};
|
||||
|
||||
if (isLoading && executions.length === 0) {
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-8">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (executions.length === 0) {
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-8">
|
||||
<div className="text-center text-gray-500">
|
||||
<p className="text-lg font-medium mb-2">No test history</p>
|
||||
<p className="text-sm">Test executions will appear here once tests are run.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div className="divide-y divide-gray-200">
|
||||
{executions.map((execution) => {
|
||||
const status = getStatus(execution);
|
||||
const isExpanded = expandedId === execution.id;
|
||||
|
||||
return (
|
||||
<div key={execution.id} className="hover:bg-gray-50 transition-colors">
|
||||
<button
|
||||
onClick={() => setExpandedId(isExpanded ? null : execution.id)}
|
||||
className="w-full px-6 py-4 text-left"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
{/* Status Badge */}
|
||||
<PackTestBadge
|
||||
status={status}
|
||||
passed={execution.passed}
|
||||
total={execution.total_tests}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{/* Test Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Version {execution.pack_version}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs rounded-full ${getTriggerBadgeColor(
|
||||
execution.trigger_reason
|
||||
)}`}
|
||||
>
|
||||
{execution.trigger_reason}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>
|
||||
{new Date(execution.execution_time).toLocaleDateString()}
|
||||
{' at '}
|
||||
{new Date(execution.execution_time).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatDuration(execution.duration_ms)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pass Rate */}
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-semibold text-gray-900">
|
||||
{(execution.pass_rate * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">pass rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<div className="grid grid-cols-4 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{execution.total_tests}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Total</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{execution.passed}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Passed</div>
|
||||
</div>
|
||||
{execution.failed > 0 && (
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{execution.failed}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Failed</div>
|
||||
</div>
|
||||
)}
|
||||
{execution.skipped > 0 && (
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-600">
|
||||
{execution.skipped}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Skipped</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Link
|
||||
to={`/packs/tests/${execution.id}`}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View Full Results →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasMore && (
|
||||
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
||||
<button
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoading}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Load More'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
267
web/src/components/packs/PackTestResult.tsx
Normal file
267
web/src/components/packs/PackTestResult.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TestCaseResult {
|
||||
name: string;
|
||||
status: 'passed' | 'failed' | 'skipped' | 'error';
|
||||
duration_ms: number;
|
||||
error_message?: string;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
}
|
||||
|
||||
interface TestSuiteResult {
|
||||
name: string;
|
||||
runner_type: string;
|
||||
total: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
duration_ms: number;
|
||||
test_cases: TestCaseResult[];
|
||||
}
|
||||
|
||||
interface PackTestResultData {
|
||||
pack_ref: string;
|
||||
pack_version: string;
|
||||
execution_time: string;
|
||||
status: string;
|
||||
total_tests: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
pass_rate: number;
|
||||
duration_ms: number;
|
||||
test_suites: TestSuiteResult[];
|
||||
}
|
||||
|
||||
interface PackTestResultProps {
|
||||
result: PackTestResultData;
|
||||
showDetails?: boolean;
|
||||
}
|
||||
|
||||
export default function PackTestResult({
|
||||
result,
|
||||
showDetails = false,
|
||||
}: PackTestResultProps) {
|
||||
const [expandedSuites, setExpandedSuites] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleSuite = (suiteName: string) => {
|
||||
setExpandedSuites((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(suiteName)) {
|
||||
next.delete(suiteName);
|
||||
} else {
|
||||
next.add(suiteName);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'passed':
|
||||
return 'text-green-600 bg-green-50';
|
||||
case 'failed':
|
||||
return 'text-red-600 bg-red-50';
|
||||
case 'skipped':
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
default:
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'passed':
|
||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
||||
case 'failed':
|
||||
return <XCircle className="w-5 h-5 text-red-600" />;
|
||||
default:
|
||||
return <Clock className="w-5 h-5 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg overflow-hidden">
|
||||
{/* Summary Header */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(result.status)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{result.status === 'passed' ? 'All Tests Passed' : 'Tests Failed'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(result.execution_time).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(
|
||||
result.status
|
||||
)}`}
|
||||
>
|
||||
{result.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Test Statistics */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">Total Tests</div>
|
||||
<div className="text-2xl font-bold">{result.total_tests}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">Passed</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{result.passed}
|
||||
</div>
|
||||
</div>
|
||||
{result.failed > 0 && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">Failed</div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{result.failed}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{result.skipped > 0 && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">Skipped</div>
|
||||
<div className="text-2xl font-bold text-gray-600">
|
||||
{result.skipped}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
|
||||
<div>
|
||||
Pass Rate:{' '}
|
||||
<span className="font-semibold">
|
||||
{(result.pass_rate * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Duration:{' '}
|
||||
<span className="font-semibold">
|
||||
{formatDuration(result.duration_ms)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Results */}
|
||||
{showDetails && result.test_suites.length > 0 && (
|
||||
<div className="p-6">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-4">
|
||||
Test Suites ({result.test_suites.length})
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{result.test_suites.map((suite) => (
|
||||
<div
|
||||
key={suite.name}
|
||||
className="border border-gray-200 rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Suite Header */}
|
||||
<button
|
||||
onClick={() => toggleSuite(suite.name)}
|
||||
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex items-center justify-between transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedSuites.has(suite.name) ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-gray-900">
|
||||
{suite.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{suite.runner_type} • {formatDuration(suite.duration_ms)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-green-600">{suite.passed} passed</span>
|
||||
{suite.failed > 0 && (
|
||||
<span className="text-red-600">{suite.failed} failed</span>
|
||||
)}
|
||||
{suite.skipped > 0 && (
|
||||
<span className="text-gray-600">
|
||||
{suite.skipped} skipped
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Test Cases */}
|
||||
{expandedSuites.has(suite.name) && (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{suite.test_cases.map((testCase, idx) => (
|
||||
<div key={idx} className="px-4 py-3 bg-white">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-2 flex-1">
|
||||
{testCase.status === 'passed' ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||
) : testCase.status === 'failed' ? (
|
||||
<XCircle className="w-4 h-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<Clock className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-sm text-gray-900">
|
||||
{testCase.name}
|
||||
</div>
|
||||
{testCase.error_message && (
|
||||
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs">
|
||||
<div className="font-semibold text-red-800 mb-1">
|
||||
Error:
|
||||
</div>
|
||||
<pre className="text-red-700 whitespace-pre-wrap break-words font-mono">
|
||||
{testCase.error_message}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{testCase.stderr && (
|
||||
<div className="mt-2 p-2 bg-gray-50 border border-gray-200 rounded text-xs">
|
||||
<div className="font-semibold text-gray-800 mb-1">
|
||||
stderr:
|
||||
</div>
|
||||
<pre className="text-gray-700 whitespace-pre-wrap break-words font-mono">
|
||||
{testCase.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 ml-4 flex-shrink-0">
|
||||
{formatDuration(testCase.duration_ms)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user