mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #912 from drudge/new-ansi-logs
feat(logs): support ansi codes
This commit is contained in:
@@ -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;
|
||||
|
||||
const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi"));
|
||||
return parts.map((part, index) =>
|
||||
part.toLowerCase() === term.toLowerCase() ? (
|
||||
<span key={index} className="bg-yellow-200 dark:bg-yellow-900">
|
||||
{part}
|
||||
if (!term) {
|
||||
const segments = parseAnsi(text);
|
||||
return segments.map((segment, index) => (
|
||||
<span key={index} className={segment.className || undefined}>
|
||||
{segment.text}
|
||||
</span>
|
||||
) : (
|
||||
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 (
|
||||
<span key={index} className={segment.className || undefined}>
|
||||
{parts.map((part, partIndex) =>
|
||||
part.toLowerCase() === term.toLowerCase() ? (
|
||||
<span
|
||||
key={partIndex}
|
||||
className="bg-yellow-200 dark:bg-yellow-900"
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const tooltip = (color: string, timestamp: string | null) => {
|
||||
@@ -104,7 +124,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
|
||||
{searchTerm ? highlightMessage(message, searchTerm) : message}
|
||||
{highlightMessage(message, searchTerm || "")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,47 @@ interface LogStyle {
|
||||
variant: LogVariant;
|
||||
color: string;
|
||||
}
|
||||
interface AnsiSegment {
|
||||
text: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const ansiToTailwind: Record<number, string> = {
|
||||
// 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<LogType, LogStyle> = {
|
||||
error: {
|
||||
@@ -150,3 +191,56 @@ export const getLogType = (message: string): LogStyle => {
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user