From 9ff4968e615856da0b7508ba30cf9ea40298d1e1 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 16 Dec 2024 14:55:02 -0500 Subject: [PATCH] feat(logs): support ansi codes --- .../dashboard/docker/logs/terminal-line.tsx | 44 ++++++-- .../components/dashboard/docker/logs/utils.ts | 106 +++++++++++++++++- 2 files changed, 134 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index cdbbb2c81..2f247e259 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -9,7 +9,7 @@ import { import { cn } from "@/lib/utils"; import { escapeRegExp } from "lodash"; import React from "react"; -import { type LogLine, getLogType } from "./utils"; +import { type LogLine, getLogType, parseAnsi } from "./utils"; interface LogLineProps { log: LogLine; @@ -33,18 +33,38 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { : "--- No time found ---"; const highlightMessage = (text: string, term: string) => { - if (!term) return text; + if (!term) { + const segments = parseAnsi(text); + return segments.map((segment, index) => ( + + {segment.text} + + )); + } - const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi")); - return parts.map((part, index) => - part.toLowerCase() === term.toLowerCase() ? ( - - {part} + // For search, we need to handle both ANSI and search highlighting + const segments = parseAnsi(text); + return segments.map((segment, index) => { + const parts = segment.text.split( + new RegExp(`(${escapeRegExp(term)})`, "gi"), + ); + return ( + + {parts.map((part, partIndex) => + part.toLowerCase() === term.toLowerCase() ? ( + + {part} + + ) : ( + part + ), + )} - ) : ( - part - ), - ); + ); + }); }; const tooltip = (color: string, timestamp: string | null) => { @@ -104,7 +124,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { - {searchTerm ? highlightMessage(message, searchTerm) : message} + {highlightMessage(message, searchTerm || "")} ); diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 409c69892..482194287 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -1,5 +1,5 @@ -export type LogType = "error" | "warning" | "success" | "info" | "debug"; -export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; export interface LogLine { rawTimestamp: string | null; @@ -12,6 +12,47 @@ interface LogStyle { variant: LogVariant; color: string; } +interface AnsiSegment { + text: string; + className: string; +} + +const ansiToTailwind: Record = { + // Reset + 0: "", + // Regular colors + 30: "text-black dark:text-gray-900", + 31: "text-red-600 dark:text-red-500", + 32: "text-green-600 dark:text-green-500", + 33: "text-yellow-600 dark:text-yellow-500", + 34: "text-blue-600 dark:text-blue-500", + 35: "text-purple-600 dark:text-purple-500", + 36: "text-cyan-600 dark:text-cyan-500", + 37: "text-gray-600 dark:text-gray-400", + // Bright colors + 90: "text-gray-500 dark:text-gray-600", + 91: "text-red-500 dark:text-red-600", + 92: "text-green-500 dark:text-green-600", + 93: "text-yellow-500 dark:text-yellow-600", + 94: "text-blue-500 dark:text-blue-600", + 95: "text-purple-500 dark:text-purple-600", + 96: "text-cyan-500 dark:text-cyan-600", + 97: "text-white dark:text-gray-300", + // Background colors + 40: "bg-black", + 41: "bg-red-600", + 42: "bg-green-600", + 43: "bg-yellow-600", + 44: "bg-blue-600", + 45: "bg-purple-600", + 46: "bg-cyan-600", + 47: "bg-white", + // Formatting + 1: "font-bold", + 2: "opacity-75", + 3: "italic", + 4: "underline", +}; const LOG_STYLES: Record = { error: { @@ -138,11 +179,68 @@ export const getLogType = (message: string): LogStyle => { if ( /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || - /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || - /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test( + lowerMessage, + ) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test( + lowerMessage, + ) ) { return LOG_STYLES.debug; } return LOG_STYLES.info; }; + +export function parseAnsi(text: string) { + const segments: { text: string; className: string }[] = []; + let currentIndex = 0; + let currentClasses: string[] = []; + + while (currentIndex < text.length) { + const escStart = text.indexOf("\x1b[", currentIndex); + + // No more escape sequences found + if (escStart === -1) { + if (currentIndex < text.length) { + segments.push({ + text: text.slice(currentIndex), + className: currentClasses.join(" "), + }); + } + break; + } + + // Add text before escape sequence + if (escStart > currentIndex) { + segments.push({ + text: text.slice(currentIndex, escStart), + className: currentClasses.join(" "), + }); + } + + const escEnd = text.indexOf("m", escStart); + if (escEnd === -1) break; + + // Handle multiple codes in one sequence (e.g., \x1b[1;31m) + const codesStr = text.slice(escStart + 2, escEnd); + const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10)); + + if (codes.includes(0)) { + // Reset all formatting + currentClasses = []; + } else { + // Add new classes for each code + for (const code of codes) { + const className = ansiToTailwind[code]; + if (className && !currentClasses.includes(className)) { + currentClasses.push(className); + } + } + } + + currentIndex = escEnd + 1; + } + + return segments; +} \ No newline at end of file