Format row outputs into tables, and only allow one log expanded at a

time
This commit is contained in:
x032205
2025-11-01 03:43:30 -04:00
parent b65078d28c
commit 0425a085fe
2 changed files with 109 additions and 9 deletions

View File

@@ -0,0 +1,95 @@
import { useState } from "react";
type TableLog = {
command?: string;
data_rows: Record<string, string | number | null>[];
total_rows?: number;
};
export const PamSessionLogOutput = ({ content }: { content: string }) => {
const [isRawView, setIsRawView] = useState(false);
let parsedContent: TableLog | null = null;
try {
const parsed = JSON.parse(content);
if (
parsed &&
typeof parsed === "object" &&
!Array.isArray(parsed) &&
parsed.data_rows &&
Array.isArray(parsed.data_rows) &&
parsed.data_rows.length > 0 &&
typeof parsed.data_rows[0] === "object" &&
parsed.data_rows[0] !== null
) {
parsedContent = parsed;
}
} catch (error) {
// Not a valid JSON or doesn't match structure, will render as plain text
}
if (parsedContent) {
const headers = Object.keys(parsedContent.data_rows[0]);
return (
<div className="font-sans">
{isRawView ? (
<div className="font-mono break-all whitespace-pre-wrap">{content}</div>
) : (
<>
{parsedContent.command && (
<div className="mb-2 font-mono">{`> ${parsedContent.command}`}</div>
)}
<div className="overflow-x-auto rounded-md border border-mineshaft-600">
<table className="w-full min-w-max text-left">
<thead className="bg-mineshaft-800">
<tr className="border-b border-mineshaft-600">
{headers.map((header) => (
<th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize">
{header.replace(/_/g, " ")}
</th>
))}
</tr>
</thead>
<tbody>
{parsedContent.data_rows.map((row, rowIndex) => (
<tr
// eslint-disable-next-line react/no-array-index-key
key={`row-${rowIndex}`}
className="border-b border-mineshaft-700 bg-mineshaft-900 last:border-b-0 hover:bg-mineshaft-800/50"
>
{headers.map((header) => (
<td key={`${header}-${rowIndex}`} className="p-2">
{String(row[header] ?? "")}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</>
)}
<div className="mt-2 flex items-center">
<button
type="button"
className="cursor-pointer text-sm text-bunker-400 underline"
onClick={(e) => {
e.stopPropagation();
setIsRawView((v) => !v);
}}
>
{isRawView ? "View Formatted" : "View Raw"}
</button>
{parsedContent.total_rows !== undefined && (
<div className="ml-auto text-right text-sm text-bunker-400">
Total rows: {parsedContent.total_rows}
</div>
)}
</div>
</div>
);
}
return <div className="font-mono break-all whitespace-pre-wrap">{content}</div>;
};

View File

@@ -3,6 +3,7 @@ import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TPamSession } from "@app/hooks/api/pam";
import { PamSessionLogOutput } from "./PamSessionLogOutput";
const formatLogContent = (text: string | null | undefined): string => {
if (!text) return "";
@@ -59,13 +60,10 @@ export const PamSessionLogsSection = ({ session }: Props) => {
const toggleExpand = (timestamp: string) => {
setExpandedLogTimestamps((prev) => {
const newSet = new Set(prev);
if (newSet.has(timestamp)) {
newSet.delete(timestamp);
} else {
newSet.add(timestamp);
if (prev.has(timestamp)) {
return new Set();
}
return newSet;
return new Set([timestamp]);
});
};
@@ -109,9 +107,16 @@ export const PamSessionLogsSection = ({ session }: Props) => {
</div>
{isExpanded && log.output && (
<div className="mt-2 border-t border-mineshaft-700 pt-2 font-mono break-all whitespace-pre-wrap text-bunker-300">
{formattedOutput}
</div>
<>
<div className="mt-2 flex items-center gap-2">
<div className="h-px w-full bg-mineshaft-400"></div>
<span className="text-xs text-mineshaft-400">OUTPUT</span>
<div className="h-px w-full bg-mineshaft-400"></div>
</div>
<div className="pt-2 text-bunker-300">
<PamSessionLogOutput content={formattedOutput} />
</div>
</>
)}
</button>
);