From b65078d28c72ddb119b437a02055992d17b28626 Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 1 Nov 2025 03:21:22 -0400 Subject: [PATCH 1/7] Fix focus ring & trim logs --- .../components/PamSessionLogsSection.tsx | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index 5f06a64e2..66d2fad8d 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -4,6 +4,52 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { TPamSession } from "@app/hooks/api/pam"; +const formatLogContent = (text: string | null | undefined): string => { + if (!text) return ""; + + let lines = text.split("\n"); + + // Find the first and last non-empty lines to trim vertical padding + let firstLineIndex = -1; + for (let i = 0; i < lines.length; i += 1) { + if (lines[i].trim() !== "") { + firstLineIndex = i; + break; + } + } + + if (firstLineIndex === -1) { + return ""; + } + + let lastLineIndex = -1; + for (let i = lines.length - 1; i >= 0; i -= 1) { + if (lines[i].trim() !== "") { + lastLineIndex = i; + break; + } + } + + lines = lines.slice(firstLineIndex, lastLineIndex + 1); + + // Determine the minimum indentation of non-empty lines + const indentations = lines + .filter((line) => line.trim() !== "") + .map((line) => { + const match = line.match(/^\s*/); + return match ? match[0].length : 0; + }); + + const minIndentation = Math.min(...indentations); + + // Remove the common indentation from all lines + if (minIndentation > 0) { + lines = lines.map((line) => line.substring(minIndentation)); + } + + return lines.join("\n"); +}; + type Props = { session: TPamSession; }; @@ -32,11 +78,14 @@ export const PamSessionLogsSection = ({ session }: Props) => { {session.commandLogs.length > 0 ? ( session.commandLogs.map((log) => { const isExpanded = expandedLogTimestamps.has(log.timestamp); + const formattedInput = formatLogContent(log.input); + const formattedOutput = formatLogContent(log.output); + return ( From 0425a085feea885bd1b2b2572dd21f6b57c98d2d Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 1 Nov 2025 03:43:30 -0400 Subject: [PATCH 2/7] Format row outputs into tables, and only allow one log expanded at a time --- .../components/PamSessionLogOutput.tsx | 95 +++++++++++++++++++ .../components/PamSessionLogsSection.tsx | 23 +++-- 2 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx new file mode 100644 index 000000000..92d173f7e --- /dev/null +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; + +type TableLog = { + command?: string; + data_rows: Record[]; + 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 ( +
+ {isRawView ? ( +
{content}
+ ) : ( + <> + {parsedContent.command && ( +
{`> ${parsedContent.command}`}
+ )} +
+ + + + {headers.map((header) => ( + + ))} + + + + {parsedContent.data_rows.map((row, rowIndex) => ( + + {headers.map((header) => ( + + ))} + + ))} + +
+ {header.replace(/_/g, " ")} +
+ {String(row[header] ?? "")} +
+
+ + )} +
+ + + {parsedContent.total_rows !== undefined && ( +
+ Total rows: {parsedContent.total_rows} +
+ )} +
+
+ ); + } + + return
{content}
; +}; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index 66d2fad8d..d620cf4cc 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -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) => { {isExpanded && log.output && ( -
- {formattedOutput} -
+ <> +
+
+ OUTPUT +
+
+
+ +
+ )} ); From 7d978443c7e0f02a6579b8ce4128b7742291055d Mon Sep 17 00:00:00 2001 From: x032205 Date: Sat, 1 Nov 2025 03:47:03 -0400 Subject: [PATCH 3/7] Move formatLogContent to a utils file --- .../components/PamSessionLogsSection.tsx | 50 +------------------ .../components/PamSessionLogsSection.utils.ts | 46 +++++++++++++++++ 2 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index d620cf4cc..9350f6cd9 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -4,52 +4,7 @@ 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 ""; - - let lines = text.split("\n"); - - // Find the first and last non-empty lines to trim vertical padding - let firstLineIndex = -1; - for (let i = 0; i < lines.length; i += 1) { - if (lines[i].trim() !== "") { - firstLineIndex = i; - break; - } - } - - if (firstLineIndex === -1) { - return ""; - } - - let lastLineIndex = -1; - for (let i = lines.length - 1; i >= 0; i -= 1) { - if (lines[i].trim() !== "") { - lastLineIndex = i; - break; - } - } - - lines = lines.slice(firstLineIndex, lastLineIndex + 1); - - // Determine the minimum indentation of non-empty lines - const indentations = lines - .filter((line) => line.trim() !== "") - .map((line) => { - const match = line.match(/^\s*/); - return match ? match[0].length : 0; - }); - - const minIndentation = Math.min(...indentations); - - // Remove the common indentation from all lines - if (minIndentation > 0) { - lines = lines.map((line) => line.substring(minIndentation)); - } - - return lines.join("\n"); -}; +import { formatLogContent } from "./PamSessionLogsSection.utils"; type Props = { session: TPamSession; @@ -77,7 +32,6 @@ export const PamSessionLogsSection = ({ session }: Props) => { session.commandLogs.map((log) => { const isExpanded = expandedLogTimestamps.has(log.timestamp); const formattedInput = formatLogContent(log.input); - const formattedOutput = formatLogContent(log.output); return ( ); }) From 050bc2d66ba480e80d35b9d3f9e7e54f03ba5ef2 Mon Sep 17 00:00:00 2001 From: Andre <120525481+x032205@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:00:09 -0400 Subject: [PATCH 6/7] Update frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../components/PamSessionLogsSection.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts index 4696331fb..d76145655 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.utils.ts @@ -35,7 +35,7 @@ export const formatLogContent = (text: string | null | undefined): string => { return match ? match[0].length : 0; }); - const minIndentation = Math.min(...indentations); + const minIndentation = indentations.length > 0 ? Math.min(...indentations) : 0; // Remove the common indentation from all lines if (minIndentation > 0) { From dbd4a9ea516ac1614e08d6ebab5ef8cc509f2a20 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 3 Nov 2025 13:27:13 -0500 Subject: [PATCH 7/7] added log searching, improved minor styling --- .../v2/HighlightText/HighlightText.tsx | 28 ++-------- .../components/PamSessionLogOutput.tsx | 19 +++++-- .../components/PamSessionLogsSection.tsx | 56 +++++++++++++++---- .../components/PamSessionRow.tsx | 34 ++++++----- 4 files changed, 84 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx index c81dab2df..92fdc1d6d 100644 --- a/frontend/src/components/v2/HighlightText/HighlightText.tsx +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -9,22 +9,10 @@ export const HighlightText = ({ }) => { if (!text) return null; - const renderTextWithNewlines = (input: string, baseKeyPrefix: string = ""): React.ReactNode[] => { - if (!input) return []; - const lines = input.split("\n"); - return lines.flatMap((line, index) => { - const nodes: React.ReactNode[] = [line]; - if (index < lines.length - 1) { - nodes.push(
); - } - return nodes; - }); - }; - const searchTerm = highlight.toLowerCase().trim(); if (!searchTerm) { - return {renderTextWithNewlines(text, "full-text")}; + return {text}; } const parts: React.ReactNode[] = []; @@ -36,16 +24,12 @@ export const HighlightText = ({ text.replace(regex, (match: string, offset: number) => { if (offset > lastIndex) { const preMatchText = text.substring(lastIndex, offset); - parts.push( - - {renderTextWithNewlines(preMatchText, `pre-${lastIndex}`)} - - ); + parts.push({preMatchText}); } parts.push( - {renderTextWithNewlines(match, `match-${offset}`)} + {match} ); @@ -56,11 +40,7 @@ export const HighlightText = ({ if (lastIndex < text.length) { const postMatchText = text.substring(lastIndex); - parts.push( - - {renderTextWithNewlines(postMatchText, `post-${lastIndex}`)} - - ); + parts.push({postMatchText}); } return parts; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx index a0cfb6cb9..99e30627d 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogOutput.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; +import { HighlightText } from "@app/components/v2/HighlightText"; import { PamResourceType } from "@app/hooks/api/pam"; type TableLog = { @@ -10,10 +11,12 @@ type TableLog = { export const PamSessionLogOutput = ({ content, - resourceType + resourceType, + search }: { content: string; resourceType: PamResourceType; + search: string; }) => { const [isRawView, setIsRawView] = useState(false); @@ -45,7 +48,9 @@ export const PamSessionLogOutput = ({ return (
{isRawView ? ( -
{content}
+
+ +
) : ( <> {parsedContent.command && ( @@ -57,7 +62,7 @@ export const PamSessionLogOutput = ({ {headers.map((header) => ( - {header.replace(/_/g, " ")} + ))} @@ -71,7 +76,7 @@ export const PamSessionLogOutput = ({ > {headers.map((header) => ( - {String(row[header] ?? "")} + ))} @@ -103,5 +108,9 @@ export const PamSessionLogOutput = ({ ); } - return
{content}
; + return ( +
+ +
+ ); }; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index b3c287380..0fbccbc9b 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -1,8 +1,10 @@ -import { useState } from "react"; -import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { useMemo, useState } from "react"; +import { faChevronRight, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Input } from "@app/components/v2"; +import { HighlightText } from "@app/components/v2/HighlightText"; import { TPamSession } from "@app/hooks/api/pam"; import { PamSessionLogOutput } from "./PamSessionLogOutput"; @@ -14,6 +16,7 @@ type Props = { export const PamSessionLogsSection = ({ session }: Props) => { const [expandedLogTimestamps, setExpandedLogTimestamps] = useState>(new Set()); + const [search, setSearch] = useState(""); const toggleExpand = (timestamp: string) => { setExpandedLogTimestamps((prev) => { @@ -24,15 +27,43 @@ export const PamSessionLogsSection = ({ session }: Props) => { }); }; + const filteredLogs = useMemo( + () => + session.commandLogs.filter((log) => { + const { input, output } = log; + + const searchValue = search.trim().toLowerCase(); + + return ( + input.toLowerCase().includes(searchValue) || output.toLowerCase().includes(searchValue) + ); + }), + [session.commandLogs, search] + ); + return (

Session Logs

-
- {session.commandLogs.length > 0 ? ( - session.commandLogs.map((log) => { - const isExpanded = expandedLogTimestamps.has(log.timestamp); + +
+ { + const newSearch = e.target.value; + setSearch(newSearch); + }} + leftIcon={} + placeholder="Search logs..." + className="flex-1 bg-mineshaft-800" + containerClassName="bg-transparent" + /> +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((log) => { + const isExpanded = search.length || expandedLogTimestamps.has(log.timestamp); const formattedInput = formatLogContent(log.input); return ( @@ -62,7 +93,7 @@ export const PamSessionLogsSection = ({ session }: Props) => { isExpanded ? "break-all whitespace-pre-wrap" : "truncate" }`} > - {formattedInput} +
{
@@ -93,8 +125,12 @@ export const PamSessionLogsSection = ({ session }: Props) => { ); }) ) : ( -
- {session.startedAt && session.endedAt ? ( +
+ {search.length ? ( +
+
No logs match search criteria
+
+ ) : (
Session logs are not yet available
@@ -103,8 +139,6 @@ export const PamSessionLogsSection = ({ session }: Props) => { If logs do not appear after some time, please contact your Gateway administrators.
- ) : ( - "No session logs" )}
)} diff --git a/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx b/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx index f730e331d..8721560c4 100644 --- a/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx +++ b/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx @@ -27,6 +27,7 @@ import { HighlightText } from "@app/components/v2/HighlightText"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam"; +import { formatLogContent } from "../../PamSessionsByIDPage/components/PamSessionLogsSection.utils"; import { PamSessionStatusBadge } from "./PamSessionStatusBadge"; type Props = { @@ -159,21 +160,28 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) = {filteredCommandLogs.length > 0 && ( - {logsToShow.map((log) => ( -
-
- - {new Date(log.timestamp).toLocaleString()} -
+ {logsToShow.map((log) => { + const formattedInput = formatLogContent(log.input); -
- + return ( +
+
+ + {new Date(log.timestamp).toLocaleString()} +
+ +
+ +
+
+ +
-
- -
-
- ))} + ); + })} {filteredCommandLogs.length > LOGS_TO_SHOW && (