mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(logs): support ansi codes
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { escapeRegExp } from "lodash";
|
import { escapeRegExp } from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { type LogLine, getLogType } from "./utils";
|
import { type LogLine, getLogType, parseAnsi } from "./utils";
|
||||||
|
|
||||||
interface LogLineProps {
|
interface LogLineProps {
|
||||||
log: LogLine;
|
log: LogLine;
|
||||||
@@ -33,18 +33,38 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
: "--- No time found ---";
|
: "--- No time found ---";
|
||||||
|
|
||||||
const highlightMessage = (text: string, term: string) => {
|
const highlightMessage = (text: string, term: string) => {
|
||||||
if (!term) return text;
|
if (!term) {
|
||||||
|
const segments = parseAnsi(text);
|
||||||
const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi"));
|
return segments.map((segment, index) => (
|
||||||
return parts.map((part, index) =>
|
<span key={index} className={segment.className || undefined}>
|
||||||
part.toLowerCase() === term.toLowerCase() ? (
|
{segment.text}
|
||||||
<span key={index} className="bg-yellow-200 dark:bg-yellow-900">
|
|
||||||
{part}
|
|
||||||
</span>
|
</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) => {
|
const tooltip = (color: string, timestamp: string | null) => {
|
||||||
@@ -104,7 +124,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
|
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
|
||||||
{searchTerm ? highlightMessage(message, searchTerm) : message}
|
{highlightMessage(message, searchTerm || "")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type LogType = "error" | "warning" | "success" | "info" | "debug";
|
export type LogType = "error" | "warning" | "success" | "info" | "debug";
|
||||||
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
|
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
|
||||||
|
|
||||||
export interface LogLine {
|
export interface LogLine {
|
||||||
rawTimestamp: string | null;
|
rawTimestamp: string | null;
|
||||||
@@ -12,6 +12,47 @@ interface LogStyle {
|
|||||||
variant: LogVariant;
|
variant: LogVariant;
|
||||||
color: string;
|
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> = {
|
const LOG_STYLES: Record<LogType, LogStyle> = {
|
||||||
error: {
|
error: {
|
||||||
@@ -138,11 +179,68 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
||||||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) ||
|
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
|
||||||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage)
|
lowerMessage,
|
||||||
|
) ||
|
||||||
|
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
|
||||||
|
lowerMessage,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return LOG_STYLES.debug;
|
return LOG_STYLES.debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
return LOG_STYLES.info;
|
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