diff --git a/model/model.go b/model/model.go index ad1ade9..35e2dc2 100644 --- a/model/model.go +++ b/model/model.go @@ -56,10 +56,10 @@ type JobEventQueue struct { JobEvent *JobEvent } type Worker struct { - Name string - Ip string - QueueName string - LastSeen time.Time + Name string `json:"name"` + Ip string `json:"id"` + QueueName string `json:"queue_name"` + LastSeen time.Time `json:"last_seen"` } type ControlEvent struct { diff --git a/server/repository/repository.go b/server/repository/repository.go index 63844e9..883c6fb 100644 --- a/server/repository/repository.go +++ b/server/repository/repository.go @@ -32,6 +32,7 @@ type Repository interface { AddVideo(ctx context.Context, video *model.Video) error WithTransaction(ctx context.Context, transactionFunc func(ctx context.Context, tx Repository) error) error GetWorker(ctx context.Context, name string) (*model.Worker, error) + GetWorkers(ctx context.Context) (*[]model.Worker, error) } type Transaction interface { @@ -154,9 +155,9 @@ func (S *SQLRepository) GetWorker(ctx context.Context, name string) (worker *mod if err != nil { return nil, err } - worker, err = S.getWorker(ctx, db, name) - return worker, err + return S.getWorker(ctx, db, name) } + func (S *SQLRepository) getWorker(ctx context.Context, db Transaction, name string) (*model.Worker, error) { rows, err := db.QueryContext(ctx, "SELECT * FROM workers WHERE name=$1", name) if err != nil { @@ -175,6 +176,31 @@ func (S *SQLRepository) getWorker(ctx context.Context, db Transaction, name stri return &worker, err } +func (S *SQLRepository) GetWorkers(ctx context.Context) (*[]model.Worker, error) { + db, err := S.getConnection(ctx) + if err != nil { + return nil, err + } + return S.getWorkers(ctx, db) +} + +func (S *SQLRepository) getWorkers(ctx context.Context, db Transaction) (*[]model.Worker, error) { + rows, err := db.QueryContext(ctx, "SELECT name, ip, queue_name, last_seen FROM workers") + if err != nil { + return nil, err + } + defer rows.Close() + + workers := []model.Worker{} + for rows.Next() { + worker := model.Worker{} + rows.Scan(&worker.Name, &worker.Ip, &worker.QueueName, &worker.LastSeen) + workers = append(workers, worker) + } + + return &workers, nil +} + func (S *SQLRepository) GetJob(ctx context.Context, uuid string) (video *model.Video, returnError error) { db, err := S.getConnection(ctx) if err != nil { diff --git a/server/scheduler/scheduler.go b/server/scheduler/scheduler.go index 783c962..28ccd3b 100644 --- a/server/scheduler/scheduler.go +++ b/server/scheduler/scheduler.go @@ -36,6 +36,7 @@ type Scheduler interface { GetUploadJobWriter(ctx context.Context, uuid string) (*UploadJobStream, error) GetDownloadJobWriter(ctx context.Context, uuid string) (*DownloadJobStream, error) GetChecksum(ctx context.Context, uuid string) (string, error) + GetWorkers(ctx context.Context) (*[]model.Worker, error) } type SchedulerConfig struct { @@ -424,6 +425,10 @@ func (R *RuntimeScheduler) GetChecksum(ctx context.Context, uuid string) (string return checksum, nil } +func (R *RuntimeScheduler) GetWorkers(ctx context.Context) (*[]model.Worker, error) { + return R.repo.GetWorkers(ctx) +} + func (S *RuntimeScheduler) stop() { } diff --git a/server/web/ui/src/App.tsx b/server/web/ui/src/App.tsx index 28f7321..ec7374f 100644 --- a/server/web/ui/src/App.tsx +++ b/server/web/ui/src/App.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import JobTable from './JobTable'; +import WorkerTable from './WorkerTable'; import useMedia from './hooks/useMedia'; import Navigation from './Navbar'; @@ -37,6 +38,12 @@ const App: React.FC = () => { </div> ); + const Workers: React.FC = () => ( + <div className="content-container"> + {showJobTable && <WorkerTable token={token} setShowJobTable={setShowJobTable} />} + </div> + ); + const [userTheme, setUserTheme] = useLocalStorage<themeSetting>(themeLocalStorageKey, 'auto'); const browserHasThemes = useMedia('(prefers-color-scheme)'); const browserWantsDarkTheme = useMedia('(prefers-color-scheme: dark)'); @@ -86,6 +93,7 @@ const App: React.FC = () => { <Routes> <Route path="/" element={<Navigate to="/jobs" replace />} /> <Route path="/jobs" element={<Jobs />} /> + <Route path="/workers" element={<Workers />} /> </Routes> </div> </Router> diff --git a/server/web/ui/src/Navbar.tsx b/server/web/ui/src/Navbar.tsx index a128aea..91de760 100644 --- a/server/web/ui/src/Navbar.tsx +++ b/server/web/ui/src/Navbar.tsx @@ -25,6 +25,11 @@ const CollapseNav: React.FC<CollapseNavProps> = ({ isOpen, className, id }) => ( Jobs </NavLink> </NavItem> + <NavItem> + <NavLink tag={Link} to="/workers"> + Workers + </NavLink> + </NavItem> <NavItem> <NavLink href="https://github.com/pando85/transcoder" title="GitHub"> <GitHubIcon /> diff --git a/server/web/ui/src/WorkerTable.tsx b/server/web/ui/src/WorkerTable.tsx new file mode 100644 index 0000000..cd8cdcb --- /dev/null +++ b/server/web/ui/src/WorkerTable.tsx @@ -0,0 +1,77 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + CircularProgress, +} from '@mui/material'; + +interface Worker { + name: string; + id: string; + queue_name: string; + last_seen: string; // Assuming 'last_seen' is a string for simplicity +} + +interface WorkerTableProps { + token: string; + setShowJobTable: React.Dispatch<React.SetStateAction<boolean>>; +} + +const WorkersTable: React.FC<WorkerTableProps> = ({ token, setShowJobTable }) => { + const [workers, setWorkers] = useState<Worker[]>([]); + const [loading, setLoading] = useState<boolean>(false); + + useEffect(() => { + const fetchWorkers = async () => { + try { + setLoading(true); + const response = await axios.get('/api/v1/workers', + { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + setWorkers(response.data); + } catch (error) { + console.error('Error fetching workers:', error); + setShowJobTable(false); + } finally { + setLoading(false); + } + }; + + fetchWorkers(); + }, []); + + return ( + <div> + <Table> + <TableHead> + <TableRow> + <TableCell>Name</TableCell> + <TableCell>ID</TableCell> + <TableCell>Queue Name</TableCell> + <TableCell>Last Seen</TableCell> + </TableRow> + </TableHead> + <TableBody> + {workers.map((worker) => ( + <TableRow key={worker.id}> + <TableCell>{worker.name}</TableCell> + <TableCell>{worker.id}</TableCell> + <TableCell>{worker.queue_name}</TableCell> + <TableCell>{worker.last_seen}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + {loading && <CircularProgress />} + </div> + ); +}; + +export default WorkersTable; diff --git a/server/web/web.go b/server/web/web.go index c5daa1a..c78c4d3 100644 --- a/server/web/web.go +++ b/server/web/web.go @@ -187,6 +187,16 @@ loop: } } +func (w *WebServer) getWorkers(c *gin.Context) { + workers, err := w.scheduler.GetWorkers(w.ctx) + if err != nil { + webError(c, err, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, workers) +} + func (w *WebServer) checksum(c *gin.Context) { id := c.Param("id") if id == "" { @@ -234,6 +244,8 @@ func NewWebServer(config WebServerConfig, scheduler scheduler.Scheduler) *WebSer api.GET("/job/:id/checksum", webServer.checksum) api.POST("/job/:id/upload", webServer.upload) + api.GET("/workers/", webServer.AuthFunc(webServer.getWorkers)) + ui.AddRoutes(r) return webServer