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: {}