Skip to content

Commit 62f8423

Browse files
authored
Correctly display XML and CSV artifacts in the UI + fallback (#7305)
1 parent 5bdbc57 commit 62f8423

File tree

4 files changed

+87
-4
lines changed

4 files changed

+87
-4
lines changed

changelog/7294.fixed.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Correctly display XML and CSV artifacts in the UI.
2+
- Added a fallback to plain text for unsupported content types.

frontend/app/src/entities/artifacts/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ export type ArtifactContentType =
66
| "application/hcl"
77
| "image/svg+xml"
88
| "text/plain"
9-
| "text/markdown";
9+
| "text/markdown"
10+
| "application/xml"
11+
| "text/csv";

frontend/app/src/entities/artifacts/ui/artifact-file.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toast } from "react-toastify";
44
import { fetchStream } from "@/shared/api/rest/fetch";
55
import { Svg } from "@/shared/components/display/svg";
66
import { CodeViewer } from "@/shared/components/editor/code/code-viewer";
7+
import { CsvTable } from "@/shared/components/editor/csv-table";
78
import { MarkdownViewer } from "@/shared/components/editor/markdown/markdown-viewer";
89
import NoDataFound from "@/shared/components/errors/no-data-found";
910
import { LoadingIndicator } from "@/shared/components/loading/loading-indicator";
@@ -30,7 +31,9 @@ const CONTENT_TYPE_CONFIG: Record<
3031
"application/hcl": { extension: "hcl", language: "hcl", label: "HCL" },
3132
"image/svg+xml": { extension: "svg", language: "svg", label: "SVG" },
3233
"text/plain": { extension: "txt", language: "text", label: "text" },
33-
};
34+
"application/xml": { extension: "xml", language: "xml", label: "XML" },
35+
"text/csv": { extension: "csv", language: "csv", label: "CSV" },
36+
} as const;
3437

3538
function FileLayout({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
3639
return (
@@ -59,7 +62,7 @@ function FileHeader({
5962
className,
6063
...props
6164
}: FileHeaderProps) {
62-
const config = CONTENT_TYPE_CONFIG[contentType];
65+
const config = CONTENT_TYPE_CONFIG[contentType] ?? CONTENT_TYPE_CONFIG["text/plain"];
6366

6467
return (
6568
<div className={classNames("flex items-center gap-1", className)} {...props}>
@@ -84,7 +87,7 @@ function FileContent({
8487
contentType: ArtifactContentType;
8588
fileContent: string;
8689
}) {
87-
const config = CONTENT_TYPE_CONFIG[contentType];
90+
const config = CONTENT_TYPE_CONFIG[contentType] ?? CONTENT_TYPE_CONFIG["text/plain"];
8891

8992
switch (contentType) {
9093
case "text/markdown": {
@@ -95,6 +98,13 @@ function FileContent({
9598
<Svg value={fileContent} className="grow rounded-lg border border-neutral-700 shadow-sm" />
9699
);
97100
}
101+
case "text/csv": {
102+
return (
103+
<ScrollArea scrollX scrollBarClassName="bg-transparent">
104+
<CsvTable content={fileContent} />
105+
</ScrollArea>
106+
);
107+
}
98108
default: {
99109
return (
100110
<ScrollArea
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { classNames } from "@/shared/utils/common";
2+
3+
interface CsvTableProps {
4+
content: string;
5+
}
6+
7+
const parseCSV = (csv: string): string[][] => {
8+
const lines = csv.trim().split("\n");
9+
return lines.map((line) => {
10+
const values: string[] = [];
11+
let current = "";
12+
let inQuotes = false;
13+
14+
for (const char of line) {
15+
if (char === '"') {
16+
inQuotes = !inQuotes;
17+
} else if (char === "," && !inQuotes) {
18+
values.push(current.trim());
19+
current = "";
20+
} else {
21+
current += char;
22+
}
23+
}
24+
values.push(current.trim());
25+
return values;
26+
});
27+
};
28+
29+
const cellStyle = "whitespace-nowrap border border-neutral-700 px-4 py-3";
30+
31+
export function CsvTable({ content }: CsvTableProps) {
32+
const rows = parseCSV(content);
33+
34+
if (!rows[0]) {
35+
return <div className="text-neutral-400 text-sm">No data available</div>;
36+
}
37+
38+
const headers = rows[0];
39+
const dataRows = rows.slice(1);
40+
41+
return (
42+
<table className="border-collapse border border-neutral-700 text-sm">
43+
<thead className="bg-neutral-900">
44+
<tr>
45+
{headers.map((header, index) => (
46+
<th
47+
key={index}
48+
className={classNames(cellStyle, "font-semibold text-xs uppercase tracking-wider")}
49+
>
50+
{header}
51+
</th>
52+
))}
53+
</tr>
54+
</thead>
55+
56+
<tbody>
57+
{dataRows.map((row, rowIndex) => (
58+
<tr key={rowIndex} className="hover:bg-neutral-900">
59+
{row.map((cell, cellIndex) => (
60+
<td key={cellIndex} className={cellStyle}>
61+
{cell}
62+
</td>
63+
))}
64+
</tr>
65+
))}
66+
</tbody>
67+
</table>
68+
);
69+
}

0 commit comments

Comments
 (0)