re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}