added log searching, improved minor styling

This commit is contained in:
x032205
2025-11-03 13:27:41 -05:00
parent 050bc2d66b
commit dbd4a9ea51
4 changed files with 84 additions and 53 deletions
@@ -9,22 +9,10 @@ export const HighlightText = ({
}) => { }) => {
if (!text) return null; 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(<br key={`${baseKeyPrefix}-br-${line}`} />);
}
return nodes;
});
};
const searchTerm = highlight.toLowerCase().trim(); const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) { if (!searchTerm) {
return <span>{renderTextWithNewlines(text, "full-text")}</span>; return <span>{text}</span>;
} }
const parts: React.ReactNode[] = []; const parts: React.ReactNode[] = [];
@@ -36,16 +24,12 @@ export const HighlightText = ({
text.replace(regex, (match: string, offset: number) => { text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) { if (offset > lastIndex) {
const preMatchText = text.substring(lastIndex, offset); const preMatchText = text.substring(lastIndex, offset);
parts.push( parts.push(<span key={`pre-${lastIndex}`}>{preMatchText}</span>);
<span key={`pre-${lastIndex}`}>
{renderTextWithNewlines(preMatchText, `pre-${lastIndex}`)}
</span>
);
} }
parts.push( parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}> <span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{renderTextWithNewlines(match, `match-${offset}`)} {match}
</span> </span>
); );
@@ -56,11 +40,7 @@ export const HighlightText = ({
if (lastIndex < text.length) { if (lastIndex < text.length) {
const postMatchText = text.substring(lastIndex); const postMatchText = text.substring(lastIndex);
parts.push( parts.push(<span key={`post-${lastIndex}`}>{postMatchText}</span>);
<span key={`post-${lastIndex}`}>
{renderTextWithNewlines(postMatchText, `post-${lastIndex}`)}
</span>
);
} }
return parts; return parts;
@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { HighlightText } from "@app/components/v2/HighlightText";
import { PamResourceType } from "@app/hooks/api/pam"; import { PamResourceType } from "@app/hooks/api/pam";
type TableLog = { type TableLog = {
@@ -10,10 +11,12 @@ type TableLog = {
export const PamSessionLogOutput = ({ export const PamSessionLogOutput = ({
content, content,
resourceType resourceType,
search
}: { }: {
content: string; content: string;
resourceType: PamResourceType; resourceType: PamResourceType;
search: string;
}) => { }) => {
const [isRawView, setIsRawView] = useState(false); const [isRawView, setIsRawView] = useState(false);
@@ -45,7 +48,9 @@ export const PamSessionLogOutput = ({
return ( return (
<div className="font-sans"> <div className="font-sans">
{isRawView ? ( {isRawView ? (
<div className="font-mono break-all whitespace-pre-wrap">{content}</div> <div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
) : ( ) : (
<> <>
{parsedContent.command && ( {parsedContent.command && (
@@ -57,7 +62,7 @@ export const PamSessionLogOutput = ({
<tr className="border-b border-mineshaft-600"> <tr className="border-b border-mineshaft-600">
{headers.map((header) => ( {headers.map((header) => (
<th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize"> <th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize">
{header.replace(/_/g, " ")} <HighlightText text={header.replace(/_/g, " ")} highlight={search} />
</th> </th>
))} ))}
</tr> </tr>
@@ -71,7 +76,7 @@ export const PamSessionLogOutput = ({
> >
{headers.map((header) => ( {headers.map((header) => (
<td key={header} className="p-2"> <td key={header} className="p-2">
{String(row[header] ?? "")} <HighlightText text={String(row[header] ?? "")} highlight={search} />
</td> </td>
))} ))}
</tr> </tr>
@@ -103,5 +108,9 @@ export const PamSessionLogOutput = ({
); );
} }
return <div className="font-mono break-all whitespace-pre-wrap">{content}</div>; return (
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
);
}; };
@@ -1,8 +1,10 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { faChevronRight, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; 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 { TPamSession } from "@app/hooks/api/pam";
import { PamSessionLogOutput } from "./PamSessionLogOutput"; import { PamSessionLogOutput } from "./PamSessionLogOutput";
@@ -14,6 +16,7 @@ type Props = {
export const PamSessionLogsSection = ({ session }: Props) => { export const PamSessionLogsSection = ({ session }: Props) => {
const [expandedLogTimestamps, setExpandedLogTimestamps] = useState<Set<string>>(new Set()); const [expandedLogTimestamps, setExpandedLogTimestamps] = useState<Set<string>>(new Set());
const [search, setSearch] = useState("");
const toggleExpand = (timestamp: string) => { const toggleExpand = (timestamp: string) => {
setExpandedLogTimestamps((prev) => { 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 ( return (
<div className="flex h-full w-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="flex h-full w-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center border-b border-mineshaft-400 pb-4"> <div className="flex items-center border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Session Logs</h3> <h3 className="text-lg font-medium text-mineshaft-100">Session Logs</h3>
</div> </div>
<div className="flex flex-col gap-2 overflow-y-auto text-xs">
{session.commandLogs.length > 0 ? ( <div className="flex gap-2">
session.commandLogs.map((log) => { <Input
const isExpanded = expandedLogTimestamps.has(log.timestamp); value={search}
onChange={(e) => {
const newSearch = e.target.value;
setSearch(newSearch);
}}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search logs..."
className="flex-1 bg-mineshaft-800"
containerClassName="bg-transparent"
/>
</div>
<div className="flex grow flex-col gap-2 overflow-y-auto text-xs">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => {
const isExpanded = search.length || expandedLogTimestamps.has(log.timestamp);
const formattedInput = formatLogContent(log.input); const formattedInput = formatLogContent(log.input);
return ( return (
@@ -62,7 +93,7 @@ export const PamSessionLogsSection = ({ session }: Props) => {
isExpanded ? "break-all whitespace-pre-wrap" : "truncate" isExpanded ? "break-all whitespace-pre-wrap" : "truncate"
}`} }`}
> >
{formattedInput} <HighlightText text={formattedInput} highlight={search} />
</div> </div>
<div <div
@@ -83,6 +114,7 @@ export const PamSessionLogsSection = ({ session }: Props) => {
<PamSessionLogOutput <PamSessionLogOutput
content={log.output} content={log.output}
resourceType={session.resourceType} resourceType={session.resourceType}
search={search}
/> />
</div> </div>
</> </>
@@ -93,8 +125,12 @@ export const PamSessionLogsSection = ({ session }: Props) => {
); );
}) })
) : ( ) : (
<div className="flex w-full grow items-center justify-center text-bunker-300"> <div className="flex grow items-center justify-center text-bunker-300">
{session.startedAt && session.endedAt ? ( {search.length ? (
<div className="text-center">
<div className="mb-2">No logs match search criteria</div>
</div>
) : (
<div className="text-center"> <div className="text-center">
<div className="mb-2">Session logs are not yet available</div> <div className="mb-2">Session logs are not yet available</div>
<div className="text-xs text-bunker-400"> <div className="text-xs text-bunker-400">
@@ -103,8 +139,6 @@ export const PamSessionLogsSection = ({ session }: Props) => {
If logs do not appear after some time, please contact your Gateway administrators. If logs do not appear after some time, please contact your Gateway administrators.
</div> </div>
</div> </div>
) : (
"No session logs"
)} )}
</div> </div>
)} )}
@@ -27,6 +27,7 @@ import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam"; import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
import { formatLogContent } from "../../PamSessionsByIDPage/components/PamSessionLogsSection.utils";
import { PamSessionStatusBadge } from "./PamSessionStatusBadge"; import { PamSessionStatusBadge } from "./PamSessionStatusBadge";
type Props = { type Props = {
@@ -159,21 +160,28 @@ export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) =
{filteredCommandLogs.length > 0 && ( {filteredCommandLogs.length > 0 && (
<Tr> <Tr>
<Td colSpan={5} className="py-3 text-xs"> <Td colSpan={5} className="py-3 text-xs">
{logsToShow.map((log) => ( {logsToShow.map((log) => {
<div key={`${id}-log-${log.timestamp}`} className="mb-4 flex flex-col last:mb-0"> const formattedInput = formatLogContent(log.input);
<div className="flex items-center gap-1.5 text-bunker-400">
<FontAwesomeIcon icon={faTerminal} className="size-3" />
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
<div className="font-mono"> return (
<HighlightText text={log.input.trim()} highlight={search} /> <div
key={`${id}-log-${log.timestamp}`}
className="mb-4 flex flex-col gap-1 last:mb-0"
>
<div className="flex items-center gap-1.5 text-bunker-400">
<FontAwesomeIcon icon={faTerminal} className="size-3" />
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={formattedInput} highlight={search} />
</div>
<div className="font-mono text-bunker-300">
<HighlightText text={log.output.trim()} highlight={search} />
</div>
</div> </div>
<div className="font-mono text-bunker-300"> );
<HighlightText text={log.output.trim()} highlight={search} /> })}
</div>
</div>
))}
{filteredCommandLogs.length > LOGS_TO_SHOW && ( {filteredCommandLogs.length > LOGS_TO_SHOW && (
<div className="mt-2"> <div className="mt-2">
<Button <Button