diff --git a/ui/src/app/api/gpu/route.ts b/ui/src/app/api/gpu/route.ts index 93a084d2..dc462115 100644 --- a/ui/src/app/api/gpu/route.ts +++ b/ui/src/app/api/gpu/route.ts @@ -47,9 +47,9 @@ async function checkNvidiaSmi(): Promise { } async function getGpuStats() { - // Get detailed GPU information in JSON format + // Get detailed GPU information in JSON format including fan speed const { stdout } = await execAsync( - 'nvidia-smi --query-gpu=index,name,driver_version,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.free,memory.used,power.draw,power.limit,clocks.current.graphics,clocks.current.memory --format=csv,noheader,nounits', + 'nvidia-smi --query-gpu=index,name,driver_version,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.free,memory.used,power.draw,power.limit,clocks.current.graphics,clocks.current.memory,fan.speed --format=csv,noheader,nounits', ); // Parse CSV output @@ -71,6 +71,7 @@ async function getGpuStats() { powerLimit, clockGraphics, clockMemory, + fanSpeed, ] = line.split(', ').map(item => item.trim()); return { @@ -95,8 +96,11 @@ async function getGpuStats() { graphics: parseInt(clockGraphics), memory: parseInt(clockMemory), }, + fan: { + speed: parseInt(fanSpeed), // Fan speed as percentage + }, }; }); return gpus; -} +} \ No newline at end of file diff --git a/ui/src/app/dashboard/page.tsx b/ui/src/app/dashboard/page.tsx index 2d989a18..e6965ac3 100644 --- a/ui/src/app/dashboard/page.tsx +++ b/ui/src/app/dashboard/page.tsx @@ -1,7 +1,9 @@ 'use client'; import GpuMonitor from '@/components/GPUMonitor'; +import JobsTable from '@/components/JobsTable'; import { TopBar, MainContent } from '@/components/layout'; +import Link from 'next/link'; export default function Dashboard() { return ( @@ -14,6 +16,15 @@ export default function Dashboard() { +
+
+

Active Jobs

+
+ View All +
+
+ +
); diff --git a/ui/src/components/GPUMonitor.tsx b/ui/src/components/GPUMonitor.tsx index f470acf0..e1306225 100644 --- a/ui/src/components/GPUMonitor.tsx +++ b/ui/src/components/GPUMonitor.tsx @@ -1,4 +1,3 @@ -// components/GpuMonitor.tsx import React, { useState, useEffect } from 'react'; import { GPUApiResponse } from '@/types'; import Loading from '@/components/Loading'; @@ -40,6 +39,30 @@ const GpuMonitor: React.FC = () => { return () => clearInterval(intervalId); }, []); + const getGridClasses = (gpuCount: number): string => { + switch (gpuCount) { + case 1: + return 'grid-cols-1'; + case 2: + return 'grid-cols-2'; + case 3: + return 'grid-cols-3'; + case 4: + return 'grid-cols-4'; + case 5: + case 6: + return 'grid-cols-3'; + case 7: + case 8: + return 'grid-cols-4'; + case 9: + case 10: + return 'grid-cols-5'; + default: + return 'grid-cols-3'; + } + }; + if (loading) { return ; } @@ -79,16 +102,18 @@ const GpuMonitor: React.FC = () => { ); } + const gridClass = getGridClasses(gpuData.gpus.length); + return ( -
+

GPU Monitor

Last updated: {lastUpdated?.toLocaleTimeString()}
-
- {gpuData.gpus.map(gpu => ( - +
+ {gpuData.gpus.map((gpu, idx) => ( + ))}
diff --git a/ui/src/components/GPUWidget.tsx b/ui/src/components/GPUWidget.tsx index f6496c6e..e9f219b1 100644 --- a/ui/src/components/GPUWidget.tsx +++ b/ui/src/components/GPUWidget.tsx @@ -1,95 +1,111 @@ -import React, { useState } from 'react'; +import React from 'react'; import { GpuInfo } from '@/types'; +import { ChevronRight, Thermometer, Zap, Clock, HardDrive, Fan, Cpu } from 'lucide-react'; interface GPUWidgetProps { gpu: GpuInfo; } export default function GPUWidget({ gpu }: GPUWidgetProps) { - // Helper to format memory values const formatMemory = (mb: number): string => { - if (mb >= 1024) { - return `${(mb / 1024).toFixed(2)} GB`; - } - return `${mb} MB`; + return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb} MB`; + }; + + const getUtilizationColor = (value: number): string => { + return value < 30 ? 'bg-emerald-500' : value < 70 ? 'bg-amber-500' : 'bg-rose-500'; }; - // Helper to determine temperature color const getTemperatureColor = (temp: number): string => { - if (temp < 50) return 'text-green-600'; - if (temp < 80) return 'text-yellow-600'; - return 'text-red-600'; + return temp < 50 ? 'text-emerald-500' : temp < 80 ? 'text-amber-500' : 'text-rose-500'; }; return ( - <> -
-
-

{gpu.name}

- GPU #{gpu.index} +
+
+
+

{gpu.name}

+ + #{gpu.index} + +
+ +
+ +
+ {/* Temperature, Fan, and Utilization Section */} +
+
+
+ +
+

Temperature

+

+ {gpu.temperature}°C +

+
+
+
+ +
+

Fan Speed

+

+ {gpu.fan.speed}% +

+
+
+
+
+
+ +

GPU Load

+ {gpu.utilization.gpu}% +
+
+
+
+
+ +

Memory

+ + {((gpu.memory.used / gpu.memory.total) * 100).toFixed(1)}% + +
+
+
+
+

+ {formatMemory(gpu.memory.used)} / {formatMemory(gpu.memory.total)} +

+
-
-
-

Temperature:

-

{gpu.temperature}°C

-
- -
-

GPU Utilization

-
-
-
-

{gpu.utilization.gpu}%

-
- -
-

Memory Utilization

-
-
-
-
- - {formatMemory(gpu.memory.used)} / {formatMemory(gpu.memory.total)} - - {((gpu.memory.used / gpu.memory.total) * 100).toFixed(1)}% -
-
- -
+ {/* Power and Clocks Section */} +
+
+
-

Power

-

- {gpu.power.draw.toFixed(1)}W / {gpu.power.limit.toFixed(1)}W +

Clock Speed

+

{gpu.clocks.graphics} MHz

+
+
+
+ +
+

Power Draw

+

+ {gpu.power.draw.toFixed(1)}W + / {gpu.power.limit.toFixed(1)}W

-
-

Memory Clock

-

{gpu.clocks.memory} MHz

-
-
- -
-
-

Graphics Clock

-

{gpu.clocks.graphics} MHz

-
-
-

Driver Version

-

{gpu.driverVersion}

-
- +
); -} +} \ No newline at end of file diff --git a/ui/src/components/JobsTable.tsx b/ui/src/components/JobsTable.tsx index 9ad755d1..ce46ea4e 100644 --- a/ui/src/components/JobsTable.tsx +++ b/ui/src/components/JobsTable.tsx @@ -2,16 +2,14 @@ import useJobsList from '@/hooks/useJobsList'; import Link from 'next/link'; import UniversalTable, { TableColumn } from '@/components/UniversalTable'; import { JobConfig } from '@/types'; -import { Eye, Trash2, Pen, Play, Pause } from 'lucide-react'; -import { Button } from '@headlessui/react'; -import { openConfirm } from '@/components/ConfirmModal'; -import { startJob, stopJob, deleteJob, getAvaliableJobActions } from '@/utils/jobs'; import JobActionBar from './JobActionBar'; -interface JobsTableProps {} +interface JobsTableProps { + onlyActive?: boolean; +} -export default function JobsTable(props: JobsTableProps) { - const { jobs, status, refreshJobs } = useJobsList(); +export default function JobsTable({ onlyActive = false }: JobsTableProps) { + const { jobs, status, refreshJobs } = useJobsList(onlyActive); const isLoading = status === 'loading'; const columns: TableColumn[] = [ diff --git a/ui/src/hooks/useJobsList.tsx b/ui/src/hooks/useJobsList.tsx index 737fe226..a1c3d2d7 100644 --- a/ui/src/hooks/useJobsList.tsx +++ b/ui/src/hooks/useJobsList.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { Job } from '@prisma/client'; -export default function useJobsList() { +export default function useJobsList(onlyActive = false) { const [jobs, setJobs] = useState([]); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); @@ -17,6 +17,9 @@ export default function useJobsList() { console.log('Error fetching jobs:', data.error); setStatus('error'); } else { + if (onlyActive) { + data.jobs = data.jobs.filter((job: Job) => job.status === 'running'); + } setJobs(data.jobs); setStatus('success'); } diff --git a/ui/src/types.ts b/ui/src/types.ts index e851c2f1..cc21aeb4 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -23,6 +23,10 @@ export interface GpuClocks { memory: number; } +export interface GpuFan { + speed: number; +} + export interface GpuInfo { index: number; name: string; @@ -32,6 +36,7 @@ export interface GpuInfo { memory: GpuMemory; power: GpuPower; clocks: GpuClocks; + fan: GpuFan; } export interface GPUApiResponse {