PrintLogTable.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { useQuery } from '@tanstack/react-query';
  2. import { useTranslation } from 'react-i18next';
  3. import { Loader2 } from 'lucide-react';
  4. import { api } from '../api/client';
  5. interface PrintLogTableProps {
  6. archiveId: number;
  7. }
  8. function formatDuration(seconds: number | null): string {
  9. if (!seconds || seconds <= 0) return '—';
  10. const h = Math.floor(seconds / 3600);
  11. const m = Math.floor((seconds % 3600) / 60);
  12. if (h > 0) return `${h}h ${m}m`;
  13. return `${m}m`;
  14. }
  15. function formatDate(isoString: string | null): string {
  16. if (!isoString) return '—';
  17. const d = new Date(isoString);
  18. return d.toLocaleString();
  19. }
  20. export function PrintLogTable({ archiveId }: PrintLogTableProps) {
  21. const { t } = useTranslation();
  22. const { data, isLoading } = useQuery({
  23. queryKey: ['archive-runs', archiveId],
  24. queryFn: () => api.getArchiveRuns(archiveId),
  25. });
  26. if (isLoading) {
  27. return (
  28. <div className="flex justify-center py-4">
  29. <Loader2 className="w-5 h-5 text-bambu-gray animate-spin" />
  30. </div>
  31. );
  32. }
  33. const runs = data?.items || [];
  34. if (runs.length === 0) {
  35. return (
  36. <p className="text-sm text-bambu-gray italic py-2">
  37. {t('archives.runLog.empty')}
  38. </p>
  39. );
  40. }
  41. return (
  42. <div className="overflow-x-auto">
  43. <table className="w-full text-xs">
  44. <thead>
  45. <tr className="text-bambu-gray border-b border-bambu-dark-tertiary">
  46. <th className="text-left py-1.5 pr-2 font-medium">{t('archives.runLog.col.date')}</th>
  47. <th className="text-left py-1.5 pr-2 font-medium">{t('archives.runLog.col.status')}</th>
  48. <th className="text-right py-1.5 pr-2 font-medium">{t('archives.runLog.col.duration')}</th>
  49. <th className="text-right py-1.5 pr-2 font-medium">{t('archives.runLog.col.filament')}</th>
  50. <th className="text-right py-1.5 font-medium">{t('archives.runLog.col.cost')}</th>
  51. </tr>
  52. </thead>
  53. <tbody>
  54. {runs.map((run) => {
  55. const statusClass =
  56. run.status === 'completed'
  57. ? 'text-bambu-green'
  58. : run.status === 'failed'
  59. ? 'text-red-400'
  60. : 'text-bambu-gray';
  61. return (
  62. <tr
  63. key={run.id}
  64. className="border-b border-bambu-dark-tertiary/40 last:border-0"
  65. >
  66. <td className="py-1.5 pr-2 text-bambu-gray-light">
  67. {formatDate(run.started_at || run.created_at)}
  68. </td>
  69. <td className={`py-1.5 pr-2 font-medium ${statusClass}`}>
  70. {t(`archives.runLog.status.${run.status}`, { defaultValue: run.status })}
  71. {run.failure_reason && (
  72. <span className="block text-[10px] text-bambu-gray font-normal">
  73. {run.failure_reason}
  74. </span>
  75. )}
  76. </td>
  77. <td className="py-1.5 pr-2 text-right text-bambu-gray-light">
  78. {formatDuration(run.duration_seconds)}
  79. </td>
  80. <td className="py-1.5 pr-2 text-right text-bambu-gray-light">
  81. {run.filament_used_grams != null
  82. ? `${run.filament_used_grams.toFixed(1)} g`
  83. : '—'}
  84. </td>
  85. <td className="py-1.5 text-right text-bambu-gray-light">
  86. {run.cost != null ? run.cost.toFixed(2) : '—'}
  87. </td>
  88. </tr>
  89. );
  90. })}
  91. </tbody>
  92. </table>
  93. </div>
  94. );
  95. }