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 f2ac0a31e1..667c073204 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx
@@ -26,7 +26,7 @@ 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";
@@ -44,16 +44,6 @@ const logsRegex = /^(\[[^\]]*\]){3}/;
// Ex: "EXP : 0"
const propertiesRegex = /^[^:]+ : [^:]+/;
-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();
-}
-
function getSyntaxProps(data: string | string[]): SyntaxHighlighterProps {
const isArray = Array.isArray(data);
const text = isArray ? data.join("\n") : data;
@@ -75,7 +65,13 @@ 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();
@@ -123,15 +119,19 @@ function Text({ studyId, filePath, filename, canEdit }: DataCompProps) {
onUploadSuccessful={handleUploadSuccessful}
/>
)}
-
+
- {isEmptyContent(text) ? (
+ {isEmptyContent(text) ? ( // TODO remove when files become editable
) : (
-
+
)}
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx
index 163235ba6f..e4bcfccc5c 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx
@@ -23,7 +23,7 @@ import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError";
import { getStudyData } from "@/services/api/study";
import { downloadFile } from "@/utils/fileUtils";
-function Unsupported({ studyId, filePath, filename }: DataCompProps) {
+function Unsupported({ studyId, filePath, filename, canEdit }: DataCompProps) {
const { t } = useTranslation();
const res = usePromiseWithSnackbarError(
@@ -56,11 +56,13 @@ function Unsupported({ studyId, filePath, filename }: DataCompProps) {
{filename}
-
+ {canEdit && (
+
+ )}
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 de7e0ca24e..9d09c246a5 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
@@ -17,7 +17,12 @@ import Text from "./Text";
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";
@@ -38,7 +43,8 @@ const componentByFileType: Record> = {
} as const;
function Data({ study, setSelectedFile, reloadTreeData, ...fileInfo }: Props) {
- const DataViewer = componentByFileType[fileInfo.fileType];
+ const fileType = getEffectiveFileType(fileInfo.filePath, fileInfo.fileType);
+ const DataViewer = componentByFileType[fileType];
return (
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
index f0e9c428b8..bf686a9439 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
@@ -20,6 +20,7 @@ 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
@@ -49,6 +50,11 @@ export interface DataCompProps extends FileInfo {
reloadTreeData: () => void;
}
+interface ContentParsingOptions {
+ filePath: string;
+ fileType: string;
+}
+
////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////
@@ -59,7 +65,14 @@ const URL_SCHEMES = {
FILE: "file://",
} as const;
-const SUPPORTED_EXTENSIONS = [".txt", ".log", ".csv", ".tsv", ".ini"] as const;
+const SUPPORTED_EXTENSIONS = [
+ ".txt",
+ ".log",
+ ".csv",
+ ".tsv",
+ ".ini",
+ ".yml",
+] as const;
// Maps file types to their corresponding icon components.
const iconByFileType: Record = {
@@ -108,15 +121,13 @@ export function getFileType(treeData: TreeData): FileType {
// 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
- if (
- treeData.startsWith(URL_SCHEMES.FILE) &&
+ return treeData.startsWith(URL_SCHEMES.FILE) &&
SUPPORTED_EXTENSIONS.some((ext) => treeData.endsWith(ext))
- ) {
- return "text";
- }
+ ? "text"
+ : "unsupported";
}
- return "unsupported";
+ return "text";
}
/**
@@ -130,7 +141,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 isOutputFolder(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 (isOutputFolder(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 (isOutputFolder(filePath) && fileType === "matrix") {
+ // Apply special handling for matrices in output folders
+ return parseResponse(content);
+ }
+
+ return content || "";
+}
+
+// !End of Matrix Display Workaround