From 11e355321db20b70c76c35b6e8fc36dbb9d97fc6 Mon Sep 17 00:00:00 2001 From: vincent van beek Date: Wed, 15 Apr 2026 16:19:02 +0200 Subject: add a job report to the scenario overview with details and time data (#406) * add a job report to the scenario overview with details and time data * create Report data class --- .../webui/components/portfolios/JobReportModal.js | 178 +++++++++++++++++++++ .../webui/components/portfolios/ScenarioTable.js | 37 +++-- 2 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 opendc-web/opendc-web-server/src/main/webui/components/portfolios/JobReportModal.js (limited to 'opendc-web/opendc-web-server/src/main/webui/components') diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/JobReportModal.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/JobReportModal.js new file mode 100644 index 00000000..5c518af7 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/JobReportModal.js @@ -0,0 +1,178 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Button, EmptyState, EmptyStateBody, EmptyStateIcon, Modal, ModalVariant, Title } from '@patternfly/react-core' +import { CheckCircleIcon } from '@patternfly/react-icons' +import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table' +import { useJobReport } from '../../data/project' + +function formatDuration(seconds) { + if (seconds < 60) { + return `${seconds}s` + } + const minutes = Math.floor(seconds / 60) + const remainingSeconds = seconds % 60 + if (minutes < 60) { + return `${minutes}m ${remainingSeconds}s` + } + const hours = Math.floor(minutes / 60) + const remainingMinutes = minutes % 60 + return `${hours}h ${remainingMinutes}m ${remainingSeconds}s` +} + +function JobReportModal({ jobId, isOpen, onClose }) { + const { data: report, isLoading } = useJobReport(jobId, { enabled: isOpen }) + + const logs = report?.logs || [] + const error = report?.error + const summary = report?.summary + const createdAt = report?.createdAt + const startedAt = report?.startedAt + + const actions = [ + , + ] + + return ( + + {isLoading &&
Loading report...
} + + {!isLoading && (createdAt || startedAt) && ( +
+ {createdAt && ( +
+ Created: {new Date(createdAt).toLocaleString()} +
+ )} + {startedAt && ( +
+ Started: {new Date(startedAt).toLocaleString()} +
+ )} +
+ )} + + {!isLoading && logs.length === 0 && !error && ( + <> + {summary && (summary.runtimeSeconds !== undefined || summary.waitTimeSeconds !== undefined) && ( +
+ {summary.runtimeSeconds !== undefined && ( +
+ Runtime: {formatDuration(summary.runtimeSeconds)} +
+ )} + {summary.waitTimeSeconds !== undefined && ( +
+ Queue Wait Time: {formatDuration(summary.waitTimeSeconds)} +
+ )} +
+ )} + + + + No warnings or errors + + This simulation completed successfully with no issues. + + + )} + + {!isLoading && error && ( +
+ + Error + +
+
+ Type: {error.type} +
+
+ Message: {error.message} +
+ {error.stackTrace && ( +
+ Stack Trace +
+                                    {error.stackTrace}
+                                
+
+ )} +
+
+ )} + + {!isLoading && logs.length > 0 && ( +
+ {summary && ( +
+ Summary: {summary.totalWarnings} warning(s), {summary.totalErrors} error(s) + {summary.runtimeSeconds !== undefined && ( + <> + {' | '} + Runtime: {formatDuration(summary.runtimeSeconds)} + + )} + {summary.waitTimeSeconds !== undefined && ( + <> + {' | '} + Queue Wait Time: {formatDuration(summary.waitTimeSeconds)} + + )} +
+ )} + + + + + + + + + + + {logs.map((log, index) => ( + + + + + + + ))} + +
TimeLevelLoggerMessage
{new Date(log.timestamp).toLocaleTimeString()} + + {log.level} + + {log.logger}{log.message}
+
+ )} +
+ ) +} + +JobReportModal.propTypes = { + jobId: PropTypes.number.isRequired, + isOpen: PropTypes.bool, + onClose: PropTypes.func.isRequired, +} + +JobReportModal.defaultProps = { + isOpen: false, +} + +export default JobReportModal diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js index b068d045..db88456b 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js @@ -23,24 +23,36 @@ import { Bullseye } from '@patternfly/react-core' import Link from 'next/link' import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table' -import React from 'react' +import React, { useState } from 'react' import { Portfolio, Status } from '../../shapes' import TableEmptyState from '../util/TableEmptyState' import ScenarioState from './ScenarioState' import { useDeleteScenario } from '../../data/project' +import JobReportModal from './JobReportModal' function ScenarioTable({ portfolio, status }) { const { mutate: deleteScenario } = useDeleteScenario() const projectId = portfolio?.project?.id const scenarios = portfolio?.scenarios ?? [] + const [reportJobId, setReportJobId] = useState(null) - const actions = ({ number }) => [ - { - title: 'Delete Scenario', - onClick: () => deleteScenario({ projectId: projectId, number }), - isDisabled: number === 0, - }, - ] + const actions = (scenario) => { + const latestJob = scenario.jobs[scenario.jobs.length - 1] + const canViewReport = latestJob && (latestJob.state === 'FINISHED' || latestJob.state === 'FAILED') + + return [ + { + title: 'View Report', + onClick: () => setReportJobId(latestJob.id), + isDisabled: !canViewReport, + }, + { + title: 'Delete Scenario', + onClick: () => deleteScenario({ projectId: projectId, number: scenario.number }), + isDisabled: scenario.number === 0, + }, + ] + } return ( @@ -49,6 +61,7 @@ function ScenarioTable({ portfolio, status }) { Name Topology Trace + Created State @@ -68,6 +81,11 @@ function ScenarioTable({ portfolio, status }) { {`${scenario.workload.trace.name} (${ scenario.workload.samplingFraction * 100 }%)`} + + {scenario.jobs && scenario.jobs.length > 0 + ? new Date(scenario.jobs[0].createdAt).toLocaleString() + : '-'} + @@ -78,7 +96,7 @@ function ScenarioTable({ portfolio, status }) { ))} {scenarios.length === 0 && ( - + )} + {reportJobId && setReportJobId(null)} />} ) } -- cgit v1.2.3