import React, { useCallback, useEffect, useState } from "react"; import { Alert, Box, Button, Chip, LinearProgress, Paper, Stack, Typography, } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; type Operation = { id: string; taskType: string; status: string; subjectType?: string | null; createdAtUtc: string; completedAtUtc?: string | null; cancellationRequestedAtUtc?: string | null; progressStage?: string | null; progressPercent?: number | null; failureCategory?: string | null; canCancel: boolean; canRetry: boolean; }; type Notification = { id: string; operationId?: string | null; kind: string; title: string; message: string; createdAtUtc: string; readAtUtc?: string | null; }; const statusLabel = (value: string) => value.replaceAll("_", " "); const dateLabel = (value: string) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? "" : date.toLocaleString(); }; export default function OperationsPage() { const [operations, setOperations] = useState([]); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busyKey, setBusyKey] = useState(null); const load = useCallback(async (showLoading = false) => { if (showLoading) setLoading(true); try { const [operationResponse, notificationResponse] = await Promise.all([ api.get("/operations?limit=50"), api.get("/notifications?limit=50"), ]); setOperations(operationResponse.data ?? []); setNotifications(notificationResponse.data ?? []); setError(null); } catch (requestError) { setError(getApiErrorMessage(requestError, "Operations could not be loaded.")); } finally { setLoading(false); } }, []); useEffect(() => { void load(); const interval = window.setInterval(() => void load(), 15000); return () => window.clearInterval(interval); }, [load]); const runAction = async (key: string, action: () => Promise, notificationsChanged = false) => { if (busyKey) return; setBusyKey(key); try { await action(); await load(); if (notificationsChanged) window.dispatchEvent(new Event("notifications-changed")); } catch (requestError) { setError(getApiErrorMessage(requestError, "The action could not be completed.")); } finally { setBusyKey(null); } }; return ( Background work survives navigation and refresh. {error ? {error} : null} {loading ? : null} Notifications {notifications.length === 0 && !loading ? No notifications. : null} {notifications.map((notification) => ( {notification.title} {notification.message} {dateLabel(notification.createdAtUtc)} {!notification.readAtUtc ? ( ) : null} ))} Operations {operations.length === 0 && !loading ? No background operations yet. : null} {operations.map((operation) => ( {operation.taskType} Started {dateLabel(operation.createdAtUtc)} {operation.progressStage ? {operation.progressStage} : null} {operation.progressPercent != null ? : null} {operation.cancellationRequestedAtUtc ? Cancellation requested. : null} {operation.failureCategory ? Failed: {statusLabel(operation.failureCategory)} : null} {operation.canCancel ? ( ) : null} {operation.canRetry ? ( ) : null} ))} ); }