diff --git a/src/components/assignment/assignment.tsx b/src/components/assignment/assignment.tsx index 6d33ef1..e43761b 100644 --- a/src/components/assignment/assignment.tsx +++ b/src/components/assignment/assignment.tsx @@ -155,7 +155,7 @@ export const AssignmentComponent = () => { refetchSubleft(); }); } - }, [lecture, assignment]); + }, [lecture, assignment, submissions.length]); if ( isLoadingAssignment || diff --git a/src/components/coursemanage/grading/edit-submission.tsx b/src/components/coursemanage/grading/edit-submission.tsx index a0f3eb6..4f6c37b 100644 --- a/src/components/coursemanage/grading/edit-submission.tsx +++ b/src/components/coursemanage/grading/edit-submission.tsx @@ -14,7 +14,6 @@ import { Assignment } from '../../../model/assignment'; import { Submission } from '../../../model/submission'; import { createOrOverrideEditRepository, - getLogs, getSubmission, pullSubmissionFiles, pushSubmissionFiles @@ -28,6 +27,7 @@ import Toolbar from '@mui/material/Toolbar'; import { showDialog } from '../../util/dialog-provider'; import { GraderLoadingButton } from '../../util/loading-button'; import { useQuery } from '@tanstack/react-query'; +import { SubmissionLogs } from '../../util/submission-logs'; export const EditSubmission = () => { const navigate = useNavigate(); @@ -51,10 +51,6 @@ export const EditSubmission = () => { getSubmission(lecture.id, assignment.id, manualGradeSubmission.id, true) }); - const { data: logs, refetch: refetchLogs } = useQuery({ - queryKey: ['logs', lecture.id, assignment.id, manualGradeSubmission.id], - queryFn: () => getLogs(lecture.id, assignment.id, manualGradeSubmission.id) - }); const { data: submissionFiles, refetch: refetchSubmissionFiles } = useQuery({ queryKey: ['submissionFiles'], @@ -67,11 +63,6 @@ export const EditSubmission = () => { await refetchSubmission(); }; - const openLogs = async () => { - setShowLogs(true); - await refetchLogs(); - }; - const pushEditedFiles = async () => { await pushSubmissionFiles(lecture, assignment, submission).then( response => { @@ -167,7 +158,7 @@ export const EditSubmission = () => { sx={{ mr: 2 }} variant="outlined" size="small" - onClick={openLogs} + onClick={() => setShowLogs(true)} > Show Logs @@ -228,25 +219,13 @@ export const EditSubmission = () => { Back - setShowLogs(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - {'Logs'} - - - {logs || 'No logs available'} - - - - - - + lectureId={lecture.id} + assignmentId={assignment.id} + submissionId={manualGradeSubmission.id} + /> ); }; diff --git a/src/components/coursemanage/grading/grading.tsx b/src/components/coursemanage/grading/grading.tsx index 967d49d..6b51e75 100644 --- a/src/components/coursemanage/grading/grading.tsx +++ b/src/components/coursemanage/grading/grading.tsx @@ -7,7 +7,6 @@ import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; import TableSortLabel from '@mui/material/TableSortLabel'; -import Typography from '@mui/material/Typography'; import Checkbox from '@mui/material/Checkbox'; import { visuallyHidden } from '@mui/utils'; import { Lecture } from '../../../model/lecture'; @@ -15,22 +14,9 @@ import { Assignment } from '../../../model/assignment'; import { Outlet, useNavigate, useOutletContext } from 'react-router-dom'; import { Submission } from '../../../model/submission'; import { utcToLocalFormat } from '../../../services/datetime.service'; -import { - Button, - Chip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - IconButton, - Stack -} from '@mui/material'; +import { Chip, IconButton, Stack } from '@mui/material'; import { SectionTitle } from '../../util/section-title'; -import { enqueueSnackbar } from 'notistack'; -import { - getAllSubmissions, - getLogs -} from '../../../services/submissions.service'; +import { getAllSubmissions } from '../../../services/submissions.service'; import { EnhancedTableToolbar } from './table-toolbar'; import EditNoteOutlinedIcon from '@mui/icons-material/EditNoteOutlined'; import { green } from '@mui/material/colors'; @@ -44,6 +30,7 @@ import { getAssignment } from '../../../services/assignments.service'; import { useQuery } from '@tanstack/react-query'; import { getLecture } from '../../../services/lectures.service'; import { extractIdsFromBreadcrumbs } from '../../util/breadcrumbs'; +import { SubmissionLogs } from '../../util/submission-logs'; /** * Calculates chip color based on submission status. @@ -305,33 +292,21 @@ export default function GradingTable() { | 'best' ); - const [showLogs, setShowLogs] = React.useState(false); - const [logs, setLogs] = React.useState(undefined); - const [search, setSearch] = React.useState(''); React.useEffect(() => { updateSubmissions(shownSubmissions); }, []); - /** - * Opens log dialog which contain autograded logs from grader service. - * @param event the click event - * @param submissionId submission for which to show logs - */ - const openLogs = (event: React.MouseEvent, submissionId: number) => { - getLogs(lecture.id, assignment.id, submissionId).then( - logs => { - setLogs(logs); - setShowLogs(true); - }, - error => { - enqueueSnackbar('No logs for submission', { - variant: 'error' - }); - } - ); - event.stopPropagation(); + const [open, setOpen] = React.useState(false); + const [submissionId, setSubmissionId] = React.useState(null); + const handleOpenLogs = (event: React.MouseEvent, id: number) => { + setSubmissionId(id); + setOpen(true); + }; + const handleCloseLogs = () => { + setSubmissionId(null); + setOpen(false); }; const updateSubmissions = (filter: 'none' | 'latest' | 'best') => { @@ -513,7 +488,10 @@ export default function GradingTable() { label={row.auto_status.split('_').join(' ')} color={getColor(row.auto_status)} clickable={true} - onClick={event => openLogs(event, row.id)} + onClick={event => { + event.stopPropagation(); // prevents the event from bubbling up to the TableRow + handleOpenLogs(event, row.id); + }} /> {getManualChip(row)} @@ -546,6 +524,15 @@ export default function GradingTable() { )} + {submissionId && ( + + )} - setShowLogs(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - {'Logs'} - - - {logs} - - - - - - ); } diff --git a/src/components/coursemanage/grading/manual-grading.tsx b/src/components/coursemanage/grading/manual-grading.tsx index 637ab12..5b9ba00 100644 --- a/src/components/coursemanage/grading/manual-grading.tsx +++ b/src/components/coursemanage/grading/manual-grading.tsx @@ -172,6 +172,14 @@ export const ManualGrading = () => { try { await autogradeSubmission(lecture, assignment, submission).then(() => { refetchSubmission(); + queryClient.invalidateQueries({ + queryKey: [ + 'submissionLogs', + lecture.id, + assignment.id, + submission.id + ] + }); }); enqueueSnackbar('Autograding submission!', { variant: 'success' diff --git a/src/components/coursemanage/grading/table-toolbar.tsx b/src/components/coursemanage/grading/table-toolbar.tsx index 7f67e65..59f6fcd 100644 --- a/src/components/coursemanage/grading/table-toolbar.tsx +++ b/src/components/coursemanage/grading/table-toolbar.tsx @@ -31,7 +31,6 @@ import { Submission } from '../../../model/submission'; import { showDialog } from '../../util/dialog-provider'; import AddIcon from '@mui/icons-material/Add'; import { Link } from 'react-router-dom'; -import { openBrowser } from '../overview/util'; import SearchIcon from '@mui/icons-material/Search'; import ClearIcon from '@mui/icons-material/Clear'; import { queryClient } from '../../../widgets/assignmentmanage'; @@ -159,6 +158,9 @@ export function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { const row = rows.find(value => value.id === id); row.auto_status = 'pending'; await autogradeSubmission(lecture, assignment, row); + await queryClient.invalidateQueries({ + queryKey: ['submissionLogs', lecture.id, assignment.id, row.id] + }); }) ); enqueueSnackbar(`Autograding ${numSelected} submissions!`, { diff --git a/src/components/util/submission-logs.tsx b/src/components/util/submission-logs.tsx new file mode 100644 index 0000000..aedf058 --- /dev/null +++ b/src/components/util/submission-logs.tsx @@ -0,0 +1,123 @@ +import { + Button, + Card, + CardActions, + CardContent, + CardHeader, + LinearProgress, + Modal, + Paper, + Typography +} from '@mui/material'; +import * as React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getLogs } from '../../services/submissions.service'; + +interface SubmissionLogsProps { + lectureId: number; + assignmentId: number; + submissionId: number; + onClose: () => void; + open: boolean; +} + +const formatLogs = (logs: string): JSX.Element[] => { + return logs.split('\n').map((line, index) => { + const timestampMatch = line.match( + /^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}]/ + ); + const logLevelMatch = line.match(/\[(INFO|WARNING|ERROR|DEBUG)]/); + + const timestamp = timestampMatch ? timestampMatch[0] : ''; + const logLevel = logLevelMatch ? logLevelMatch[0] : ''; + const message = line.replace(timestamp, '').replace(logLevel, '').trim(); + + let logLevelColor = 'white'; + if (logLevelMatch) { + switch (logLevelMatch[1]) { + case 'INFO': + logLevelColor = 'lightGreen'; + break; + case 'WARNING': + logLevelColor = 'orange'; + break; + case 'ERROR': + logLevelColor = 'red'; + break; + case 'DEBUG': + logLevelColor = 'lightBlue'; + break; + default: + logLevelColor = 'white'; + } + } + + return ( +
+ {timestamp && {timestamp}}{' '} + {logLevel && {logLevel}}{' '} + {message} +
+ ); + }); +}; + +export const SubmissionLogs = (props: SubmissionLogsProps) => { + const { data: logs, isLoading: isLoadingLogs } = useQuery({ + queryKey: [ + 'submissionLogs', + props.lectureId, + props.assignmentId, + props.submissionId + ], + queryFn: () => + getLogs(props.lectureId, props.assignmentId, props.submissionId) + }); + + if (isLoadingLogs) { + return ( +
+ + + +
+ ); + } + return ( + + + + + {logs && logs.length > 0 ? ( + formatLogs(logs) + ) : ( + No Logs found. + )} + + + + + + + + ); +};