diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
index 2f247e25..c25acc67 100644
--- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
+++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
@@ -7,9 +7,10 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
+import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash";
import React from "react";
-import { type LogLine, getLogType, parseAnsi } from "./utils";
+import { type LogLine, getLogType } from "./utils";
interface LogLineProps {
log: LogLine;
@@ -17,6 +18,8 @@ interface LogLineProps {
searchTerm?: string;
}
+const fancyAnsi = new FancyAnsi();
+
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const { timestamp, message, rawTimestamp } = log;
const { type, variant, color } = getLogType(message);
@@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const highlightMessage = (text: string, term: string) => {
if (!term) {
- const segments = parseAnsi(text);
- return segments.map((segment, index) => (
-
- {segment.text}
-
- ));
+ return (
+
+ );
}
- // 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
- ),
- )}
-
- );
- });
+ const htmlContent = fancyAnsi.toHtml(text);
+ const modifiedContent = htmlContent.replace(
+ /]*)>([^<]*)<\/span>/g,
+ (match, attrs, content) => {
+ const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
+ if (!content.match(searchRegex)) return match;
+
+ const segments = content.split(searchRegex);
+ const wrappedSegments = segments
+ .map((segment: string) =>
+ segment.toLowerCase() === term.toLowerCase()
+ ? `${segment}`
+ : segment,
+ )
+ .join("");
+
+ return `${wrappedSegments}`;
+ },
+ );
+
+ return (
+
+ );
};
const tooltip = (color: string, timestamp: string | null) => {
diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts
index 48219428..cf0b30bb 100644
--- a/apps/dokploy/components/dashboard/docker/logs/utils.ts
+++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts
@@ -12,47 +12,6 @@ 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: {
@@ -191,56 +150,3 @@ 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;
-}
\ No newline at end of file
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json
index 39c075a6..a16dedac 100644
--- a/apps/dokploy/package.json
+++ b/apps/dokploy/package.json
@@ -35,8 +35,6 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
- "react-confetti-explosion":"2.1.2",
- "@stepperize/react": "4.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@@ -64,6 +62,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
+ "@stepperize/react": "4.0.1",
"@stripe/stripe-js": "4.8.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0",
@@ -87,6 +86,7 @@
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
+ "fancy-ansi": "^0.1.3",
"i18next": "^23.16.4",
"input-otp": "^1.2.4",
"js-cookie": "^3.0.5",
@@ -104,6 +104,7 @@
"postgres": "3.4.4",
"public-ip": "6.0.2",
"react": "18.2.0",
+ "react-confetti-explosion": "2.1.2",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.1.0",
diff --git a/apps/dokploy/tailwind.config.ts b/apps/dokploy/tailwind.config.ts
index c4fa88ec..45b529af 100644
--- a/apps/dokploy/tailwind.config.ts
+++ b/apps/dokploy/tailwind.config.ts
@@ -87,7 +87,7 @@ const config = {
},
},
},
- plugins: [require("tailwindcss-animate")],
+ plugins: [require("tailwindcss-animate"), require("fancy-ansi/plugin")],
} satisfies Config;
export default config;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62ba2f56..df2dc8e0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -250,6 +250,9 @@ importers:
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
+ fancy-ansi:
+ specifier: ^0.1.3
+ version: 0.1.3
i18next:
specifier: ^23.16.4
version: 23.16.5
@@ -874,9 +877,11 @@ packages:
'@esbuild-kit/core-utils@3.3.2':
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
+ deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild-kit/esm-loader@2.6.5':
resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
+ deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
@@ -4401,6 +4406,9 @@ packages:
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
engines: {node: '>=6'}
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@@ -4460,6 +4468,9 @@ packages:
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
+ fancy-ansi@0.1.3:
+ resolution: {integrity: sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==}
+
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
@@ -10735,6 +10746,8 @@ snapshots:
escalade@3.1.2: {}
+ escape-html@1.0.3: {}
+
escape-string-regexp@1.0.5: {}
escape-string-regexp@5.0.0: {}
@@ -10795,6 +10808,10 @@ snapshots:
dependencies:
type: 2.7.3
+ fancy-ansi@0.1.3:
+ dependencies:
+ escape-html: 1.0.3
+
fast-copy@3.0.2: {}
fast-deep-equal@2.0.1: {}