PrintLogTable.tsx 3.6 KB

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