diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json
index 952091b0d4..df948d3920 100644
--- a/webapp/public/locales/en/main.json
+++ b/webapp/public/locales/en/main.json
@@ -251,7 +251,7 @@
"study.district": "District",
"study.bindingconstraints": "Binding Constraints",
"study.debug": "Debug",
- "study.debug.file.image": "Image file",
+ "study.debug.file.unsupported": "Unsupported file type",
"study.debug.file.deleteConfirm.title": "Delete File?",
"study.debug.file.deleteConfirm.message": "Are you sure you want to delete the file?",
"study.debug.folder.empty": "Folder is empty",
diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json
index 81ffca6679..3dacacb6e0 100644
--- a/webapp/public/locales/fr/main.json
+++ b/webapp/public/locales/fr/main.json
@@ -251,8 +251,8 @@
"study.district": "District",
"study.bindingconstraints": "Contraintes Couplantes",
"study.debug": "Debug",
- "study.debug.file.image": "Fichier image",
"study.debug.folder.empty": "Le dossier est vide",
+ "study.debug.file.unsupported": "Type de fichier non supporté",
"study.debug.file.deleteConfirm.title": "Supprimer le fichier ?",
"study.debug.file.deleteConfirm.message": "Êtes-vous sûr de vouloir supprimer le fichier ?",
"study.debug.folder.upload.replaceFileConfirm.title": "Remplacer le fichier ?",
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx
deleted file mode 100644
index 23aa34e993..0000000000
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2024, RTE (https://www.rte-france.com)
- *
- * See AUTHORS.txt
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * SPDX-License-Identifier: MPL-2.0
- *
- * This file is part of the Antares project.
- */
-
-import { useTranslation } from "react-i18next";
-import EmptyView from "../../../../../common/page/SimpleContent";
-import ImageIcon from "@mui/icons-material/Image";
-import { Filename, Flex, Menubar } from "./styles";
-import type { DataCompProps } from "../utils";
-
-function Image({ filename }: DataCompProps) {
- const { t } = useTranslation();
-
- return (
-
-
- {filename}
-
-
-
- );
-}
-
-export default Image;
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx
index a140fe71e8..26a211251d 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx
@@ -15,7 +15,7 @@
import Matrix from "../../../../../common/Matrix";
import type { DataCompProps } from "../utils";
-function DebugMatrix({ studyId, filename, filePath, canEdit }: DataCompProps) {
+function DebugMatrix({ filename, filePath, canEdit }: DataCompProps) {
return ;
}
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx
index 7380186bc3..1f7a7f4909 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx
@@ -26,11 +26,13 @@ import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintex
import ini from "react-syntax-highlighter/dist/esm/languages/hljs/ini";
import properties from "react-syntax-highlighter/dist/esm/languages/hljs/properties";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
-import type { DataCompProps } from "../utils";
+import { isEmptyContent, parseContent, type DataCompProps } from "../utils";
import DownloadButton from "../../../../../common/buttons/DownloadButton";
import { downloadFile } from "../../../../../../utils/fileUtils";
import { Filename, Flex, Menubar } from "./styles";
import UploadFileButton from "../../../../../common/buttons/UploadFileButton";
+import EmptyView from "@/components/common/page/SimpleContent";
+import GridOffIcon from "@mui/icons-material/GridOff";
SyntaxHighlighter.registerLanguage("xml", xml);
SyntaxHighlighter.registerLanguage("plaintext", plaintext);
@@ -63,15 +65,24 @@ function getSyntaxProps(data: string | string[]): SyntaxHighlighterProps {
};
}
-function Text({ studyId, filePath, filename, canEdit }: DataCompProps) {
+function Text({
+ studyId,
+ filePath,
+ filename,
+ fileType,
+ canEdit,
+}: DataCompProps) {
const { t } = useTranslation();
const theme = useTheme();
const res = usePromiseWithSnackbarError(
- () => getStudyData(studyId, filePath),
+ () =>
+ getStudyData(studyId, filePath).then((text) =>
+ parseContent(text, { filePath, fileType }),
+ ),
{
errorMessage: t("studies.error.retrieveData"),
- deps: [studyId, filePath],
+ deps: [studyId, filePath, fileType],
},
);
@@ -113,22 +124,34 @@ function Text({ studyId, filePath, filename, canEdit }: DataCompProps) {
)}
-
-
+ ) : (
+
-
+ >
+
+
+ )}
)}
/>
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx
new file mode 100644
index 0000000000..e4bcfccc5c
--- /dev/null
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2024, RTE (https://www.rte-france.com)
+ *
+ * See AUTHORS.txt
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This file is part of the Antares project.
+ */
+
+import { useTranslation } from "react-i18next";
+import EmptyView from "../../../../../common/page/SimpleContent";
+import BlockIcon from "@mui/icons-material/Block";
+import { Filename, Flex, Menubar } from "./styles";
+import type { DataCompProps } from "../utils";
+import DownloadButton from "@/components/common/buttons/DownloadButton";
+import UploadFileButton from "@/components/common/buttons/UploadFileButton";
+import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError";
+import { getStudyData } from "@/services/api/study";
+import { downloadFile } from "@/utils/fileUtils";
+
+function Unsupported({ studyId, filePath, filename, canEdit }: DataCompProps) {
+ const { t } = useTranslation();
+
+ const res = usePromiseWithSnackbarError(
+ () => getStudyData(studyId, filePath),
+ {
+ errorMessage: t("studies.error.retrieveData"),
+ deps: [studyId, filePath],
+ },
+ );
+
+ ////////////////////////////////////////////////////////////////
+ // Event Handlers
+ ////////////////////////////////////////////////////////////////
+
+ const handleDownload = () => {
+ if (res.data) {
+ downloadFile(res.data, filename);
+ }
+ };
+
+ const handleUploadSuccessful = () => {
+ res.reload();
+ };
+
+ ////////////////////////////////////////////////////////////////
+ // JSX
+ ////////////////////////////////////////////////////////////////
+
+ return (
+
+
+ {filename}
+ {canEdit && (
+
+ )}
+
+
+
+
+ );
+}
+
+export default Unsupported;
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
index f387b7ef9c..9d09c246a5 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
@@ -12,15 +12,21 @@
* This file is part of the Antares project.
*/
+import { ComponentType } from "react";
import Text from "./Text";
-import Image from "./Image";
-import Json from "./Json";
+import Unsupported from "./Unsupported";
import Matrix from "./Matrix";
import Folder from "./Folder";
-import { canEditFile, type FileInfo, type FileType } from "../utils";
+import {
+ canEditFile,
+ getEffectiveFileType,
+ type FileInfo,
+ type FileType,
+} from "../utils";
import type { DataCompProps } from "../utils";
import ViewWrapper from "../../../../../common/page/ViewWrapper";
import type { StudyMetadata } from "../../../../../../common/types";
+import Json from "./Json";
interface Props extends FileInfo {
study: StudyMetadata;
@@ -28,20 +34,16 @@ interface Props extends FileInfo {
reloadTreeData: () => void;
}
-type DataComponent = React.ComponentType;
-
-const componentByFileType: Record = {
+const componentByFileType: Record> = {
matrix: Matrix,
json: Json,
text: Text,
- image: Image,
+ unsupported: Unsupported,
folder: Folder,
} as const;
-function Data(props: Props) {
- const { study, setSelectedFile, reloadTreeData, ...fileInfo } = props;
- const { fileType, filePath } = fileInfo;
- const canEdit = canEditFile(study, filePath);
+function Data({ study, setSelectedFile, reloadTreeData, ...fileInfo }: Props) {
+ const fileType = getEffectiveFileType(fileInfo.filePath, fileInfo.fileType);
const DataViewer = componentByFileType[fileType];
return (
@@ -49,7 +51,7 @@ function Data(props: Props) {
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
index b1d8653f07..d546a1e4dc 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
@@ -14,12 +14,13 @@
import DataObjectIcon from "@mui/icons-material/DataObject";
import TextSnippetIcon from "@mui/icons-material/TextSnippet";
-import ImageIcon from "@mui/icons-material/Image";
+import BlockIcon from "@mui/icons-material/Block";
import FolderIcon from "@mui/icons-material/Folder";
import DatasetIcon from "@mui/icons-material/Dataset";
import { SvgIconComponent } from "@mui/icons-material";
import * as RA from "ramda-adjunct";
import type { StudyMetadata } from "../../../../../common/types";
+import { MatrixDataDTO } from "@/components/common/Matrix/shared/types";
////////////////////////////////////////////////////////////////
// Types
@@ -33,7 +34,7 @@ export interface TreeFolder {
export type TreeData = TreeFolder | TreeFile;
-export type FileType = "json" | "matrix" | "text" | "image" | "folder";
+export type FileType = "json" | "matrix" | "text" | "folder" | "unsupported";
export interface FileInfo {
fileType: FileType;
@@ -49,19 +50,43 @@ export interface DataCompProps extends FileInfo {
reloadTreeData: () => void;
}
+interface ContentParsingOptions {
+ filePath: string;
+ fileType: string;
+}
+
////////////////////////////////////////////////////////////////
-// File Info
+// Constants
////////////////////////////////////////////////////////////////
+const URL_SCHEMES = {
+ MATRIX: ["matrix://", "matrixfile://"],
+ JSON: "json://",
+ FILE: "file://",
+} as const;
+
+const SUPPORTED_EXTENSIONS = [
+ ".txt",
+ ".log",
+ ".csv",
+ ".tsv",
+ ".ini",
+ ".yml",
+] as const;
+
// Maps file types to their corresponding icon components.
const iconByFileType: Record = {
matrix: DatasetIcon,
json: DataObjectIcon,
text: TextSnippetIcon,
- image: ImageIcon,
folder: FolderIcon,
+ unsupported: BlockIcon,
} as const;
+////////////////////////////////////////////////////////////////
+// Functions
+////////////////////////////////////////////////////////////////
+
/**
* Gets the icon component for a given file type.
*
@@ -83,26 +108,33 @@ export function isFolder(treeData: TreeData): treeData is TreeFolder {
* @returns The corresponding file type.
*/
export function getFileType(treeData: TreeData): FileType {
+ if (isFolder(treeData)) {
+ return "folder";
+ }
+
if (typeof treeData === "string") {
- if (
- treeData.startsWith("matrix://") ||
- treeData.startsWith("matrixfile://")
- ) {
+ if (URL_SCHEMES.MATRIX.some((scheme) => treeData.startsWith(scheme))) {
return "matrix";
}
- if (treeData.startsWith("json://") || treeData.endsWith(".json")) {
+
+ if (treeData.startsWith(URL_SCHEMES.JSON)) {
return "json";
}
- if (treeData.startsWith("file://") && treeData.endsWith(".ico")) {
- return "image";
- }
+
+ // Handle regular files with file:// prefix
+ // All files except matrices and json-formatted content use this prefix
+ // We filter to only allow extensions that can be properly displayed (.txt, .log, .csv, .tsv, .ini)
+ // Other extensions (like .RDS or .xlsx) are marked as unsupported since they can't be shown in the UI
+ return treeData.startsWith(URL_SCHEMES.FILE) &&
+ SUPPORTED_EXTENSIONS.some((ext) =>
+ treeData.toLowerCase().endsWith(ext.toLowerCase()),
+ )
+ ? "text"
+ : "unsupported";
}
- return isFolder(treeData) ? "folder" : "text";
-}
-////////////////////////////////////////////////////////////////
-// Rights
-////////////////////////////////////////////////////////////////
+ return "text";
+}
/**
* Checks if a study's file can be edited.
@@ -115,7 +147,138 @@ export function canEditFile(study: StudyMetadata, filePath: string): boolean {
return (
!study.archived &&
(filePath === "user" || filePath.startsWith("user/")) &&
- // To remove when Xpansion tool configuration will be moved to "input/expansion" directory
+ // TODO: remove when Xpansion tool configuration will be moved to "input/expansion" directory
!(filePath === "user/expansion" || filePath.startsWith("user/expansion/"))
);
}
+
+/**
+ * Checks if a file path is within the output folder
+ *
+ * @param path - The file path to check
+ * @returns Whether the path is in the output folder
+ */
+export function isInOutputFolder(path: string): boolean {
+ return path.startsWith("output/");
+}
+
+/**
+ * Determines if .txt files content is empty
+ *
+ * @param text - Content of .txt to check
+ * @returns boolean indicating if content is effectively empty
+ */
+export function isEmptyContent(text: string | string[]): boolean {
+ if (Array.isArray(text)) {
+ return (
+ !text || text.every((line) => typeof line === "string" && !line.trim())
+ );
+ }
+
+ return typeof text === "string" && !text.trim();
+}
+
+/**
+ * !Temporary workaround for matrix data display in output folders.
+ *
+ * Context:
+ * In output folders, matrix data can be returned by the API in two different formats:
+ * 1. As an unparsed JSON string containing the matrix data
+ * 2. As an already parsed MatrixDataDTO object
+ *
+ * This inconsistency exists because the API's matrix parsing behavior differs between
+ * output folders and other locations. Additionally, there's a UI requirement to display
+ * matrices from output folders as raw text rather than formatted tables.
+ *
+ * The workaround consists of three functions:
+ * 1. getEffectiveFileType: Forces matrix files in output folders to use text display
+ * 2. parseResponse: Handles the dual format nature of the API response
+ * 3. parseContent: Orchestrates the parsing logic based on file location and type
+ *
+ * TODO: This temporary solution will be removed once:
+ * - The API provides consistent matrix parsing across all folders
+ * - UI requirements for matrix display are finalized
+ */
+
+/**
+ * Forces matrix files in output folders to be displayed as text
+ * This is necessary because matrices in output folders need to be shown
+ * in their raw format rather than as formatted tables
+ *
+ * @param filePath - Path to the file being displayed
+ * @param originalType - Original file type as determined by the system
+ * @returns Modified file type (forces 'text' for matrices in output folders)
+ */
+export function getEffectiveFileType(
+ filePath: string,
+ originalType: FileType,
+): FileType {
+ if (isInOutputFolder(filePath) && originalType === "matrix") {
+ return "text";
+ }
+
+ return originalType;
+}
+
+/**
+ * Formats a 2D number array into a string representation
+ *
+ * @param matrix - 2D array of numbers to format
+ * @returns String representation of the matrix
+ */
+function formatMatrixToString(matrix: number[][]): string {
+ return matrix
+ .map((row) => row.map((val) => val.toString()).join("\t"))
+ .join("\n");
+}
+
+/**
+ * Handles parsing of matrix data from the API, dealing with both
+ * string and pre-parsed object formats
+ *
+ * @param res - API response containing matrix data (either MatrixDataDTO or string)
+ * @returns Extracted matrix data as a string
+ */
+function parseResponse(res: string | MatrixDataDTO): string {
+ if (typeof res === "object") {
+ // Handle case where API has already parsed the JSON into MatrixDataDTO
+ return formatMatrixToString(res.data);
+ }
+
+ try {
+ // Handle case where API returns unparsed JSON string
+ // Replace special numeric values with their string representations
+ const sanitizedJson = res
+ .replace(/NaN/g, '"NaN"')
+ .replace(/Infinity/g, '"Infinity"');
+
+ const parsed = JSON.parse(sanitizedJson);
+ return formatMatrixToString(parsed.data);
+ } catch (e) {
+ // If JSON parsing fails, assume it's plain text
+ return res;
+ }
+}
+
+/**
+ * Main content parsing function that orchestrates the matrix display workaround
+ *
+ * @param content - Raw content from the API (either string or parsed object)
+ * @param options - Configuration options including file path and type
+ * @returns Processed content ready for display
+ */
+export function parseContent(
+ content: string,
+ options: ContentParsingOptions,
+): string {
+ const { filePath, fileType } = options;
+
+ if (isInOutputFolder(filePath) && fileType === "matrix") {
+ // Apply special handling for matrices in output folders
+ return parseResponse(content);
+ }
+
+ return content || "";
+}
+
+// !End of Matrix Display Workaround