diff --git a/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java b/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java new file mode 100644 index 000000000..be54e7471 --- /dev/null +++ b/diagram-converter/webapp/src/main/java/io/camunda/migration/diagram/converter/webapp/ConverterExceptionHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.diagram.converter.webapp; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.multipart.MultipartException; + +@ControllerAdvice +public class ConverterExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ConverterExceptionHandler.class); + + @ExceptionHandler(MultipartException.class) + public void handleMultipartException(MultipartException ex, HttpServletResponse response) + throws IOException { + Throwable rootCause = getRootCause(ex); + + if (rootCause instanceof FileCountLimitExceededException fileCountEx) { + LOG.warn("File count limit exceeded: {}", rootCause.getMessage()); + writeJsonError( + response, + HttpStatus.PAYLOAD_TOO_LARGE, + "FILE_COUNT_LIMIT_EXCEEDED", + fileCountEx.getLimit()); + return; + } + + LOG.error("Multipart request processing failed", ex); + writeJsonError(response, HttpStatus.BAD_REQUEST, "MULTIPART_ERROR", -1); + } + + private void writeJsonError( + HttpServletResponse response, HttpStatus status, String errorCode, long maxPartCount) + throws IOException { + response.setStatus(status.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + String json = + "{\"errorCode\":\"" + + errorCode + + "\",\"status\":" + + status.value() + + ",\"maxPartCount\":" + + maxPartCount + + "}"; + response.getWriter().write(json); + } + + private Throwable getRootCause(Throwable throwable) { + Throwable cause = throwable; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause; + } +} diff --git a/diagram-converter/webapp/src/main/javascript/src/App.jsx b/diagram-converter/webapp/src/main/javascript/src/App.jsx index 3723f52e9..2f0eed68e 100644 --- a/diagram-converter/webapp/src/main/javascript/src/App.jsx +++ b/diagram-converter/webapp/src/main/javascript/src/App.jsx @@ -11,7 +11,7 @@ import { ProgressIndicator, ProgressStep, Button, - Callout, + ActionableNotification, DataTable, Table, TableHead, @@ -46,6 +46,9 @@ function App() { const [previewTableHeader, setPreviewTableHeader] = useState([]); const [previewTableRows, setPreviewTableRows] = useState([]); + const [downloadError, setDownloadError] = useState(null); + const [downloadErrorTitle, setDownloadErrorTitle] = useState(""); + const [showConfig, setShowConfig] = useState(false); const [configOptions, setConfigOptions] = useState({ defaultJobType: "camunda-7-job", @@ -199,10 +202,40 @@ function App() { setValidFiles(validFiles); } + function buildErrorMessage(errorBody) { + switch (errorBody.errorCode) { + case "FILE_COUNT_LIMIT_EXCEEDED": + return <> + Too many files uploaded. The online version supports up to {errorBody.maxPartCount} parts per request. + {" "}To learn how to run the diagram converter locally with a custom limit, consult the{" "} + diagram converter guide. + ; + default: + return "Download failed. Please try again."; + } + } + + async function handleDownloadResponse(filename, response, title) { + if (!response.ok) { + let errorMessage = "Download failed. Please try again."; + try { + const errorBody = await response.json(); + errorMessage = buildErrorMessage(errorBody); + } catch { + // Response body is not JSON, use default message + } + setDownloadErrorTitle(title); + setDownloadError(errorMessage); + return; + } + setDownloadError(null); + await download1(filename, response); + } + async function downloadXLS() { const formData = createFormData(validFiles); - //validFiles.forEach((file) => formData.append("file", file)); - await download1("analysis.xlsx", + await handleDownloadResponse("analysis.xlsx", await fetch(baseUrl + "/check", { body: formData, method: "POST", @@ -210,30 +243,31 @@ function App() { Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, - }) + }), + "Downloading XLSX failed" ); } async function downloadCSV() { const formData = createFormData(validFiles); - //validFiles.forEach((file) => formData.append("file", file)); - await download1("analysis.csv", + await handleDownloadResponse("analysis.csv", await fetch(baseUrl + "/check", { body: formData, method: "POST", headers: { Accept: "text/csv", }, - }) + }), + "Downloading CSV failed" ); } async function downloadZIP() { const formData = createFormData(validFiles); - //validFiles.forEach((file) => formData.append("file", file)); - await download1("converted-models.zip", + await handleDownloadResponse("converted-models.zip", await fetch(baseUrl + "/convertBatch", { body: formData, method: "POST", - }) + }), + "Downloading ZIP failed" ); } @@ -522,6 +556,17 @@ function App() { } /> ))} + {downloadError && ( + setDownloadError(null)} + className="download-error-notification" + > + {downloadError} + + )}