mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(logs): improvements based on feedback
This commit is contained in:
@@ -1,9 +1,6 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import React, { useEffect, useRef } from "react";
|
import { Input } from "@/components/ui/input";
|
||||||
import { getLogType, LogLine, parseLogs } from "./utils";
|
|
||||||
import { TerminalLine } from "./terminal-line";
|
|
||||||
import { Download as DownloadIcon } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -11,8 +8,11 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Download as DownloadIcon } from "lucide-react";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { TerminalLine } from "./terminal-line";
|
||||||
|
import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
@@ -30,7 +30,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: !!containerId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [rawLogs, setRawLogs] = React.useState("");
|
const [rawLogs, setRawLogs] = React.useState("");
|
||||||
@@ -132,7 +132,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
const logContent = filteredLogs
|
const logContent = filteredLogs
|
||||||
.map(
|
.map(
|
||||||
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||||
`${timestamp?.toISOString() || "No timestamp"} ${message}`
|
`${timestamp?.toISOString() || "No timestamp"} ${message}`,
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
@@ -190,19 +190,13 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
onChange={handleLines}
|
onChange={handleLines}
|
||||||
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
type="search"
|
|
||||||
placeholder="Search logs..."
|
|
||||||
value={search}
|
|
||||||
onChange={handleSearch}
|
|
||||||
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
|
||||||
/>
|
|
||||||
<Select value={since} onValueChange={handleSince}>
|
<Select value={since} onValueChange={handleSince}>
|
||||||
<SelectTrigger className="w-[180px] h-9">
|
<SelectTrigger className="w-[180px] h-9">
|
||||||
<SelectValue placeholder="Time filter" />
|
<SelectValue placeholder="Time filter" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1h">Last 1 hour</SelectItem>
|
<SelectItem value="1h">Last hour</SelectItem>
|
||||||
<SelectItem value="6h">Last 6 hours</SelectItem>
|
<SelectItem value="6h">Last 6 hours</SelectItem>
|
||||||
<SelectItem value="24h">Last 24 hours</SelectItem>
|
<SelectItem value="24h">Last 24 hours</SelectItem>
|
||||||
<SelectItem value="168h">Last 7 days</SelectItem>
|
<SelectItem value="168h">Last 7 days</SelectItem>
|
||||||
@@ -233,6 +227,14 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search logs..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -46,10 +46,7 @@ export const ShowDockerModalLogs = ({
|
|||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId
|
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
||||||
containerId={containerId || ""}
|
|
||||||
serverId={serverId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getLogType, type LogLine } from "./utils";
|
import { escapeRegExp } from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { escapeRegExp } from 'lodash';
|
import { type LogLine, getLogType } from "./utils";
|
||||||
|
|
||||||
interface LogLineProps {
|
interface LogLineProps {
|
||||||
log: LogLine;
|
log: LogLine;
|
||||||
@@ -10,18 +16,17 @@ interface LogLineProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TerminalLine({ log, searchTerm }: LogLineProps) {
|
export function TerminalLine({ log, searchTerm }: LogLineProps) {
|
||||||
const { timestamp, message } = log;
|
const { timestamp, message, rawTimestamp } = log;
|
||||||
const { type, variant, color } = getLogType(message);
|
const { type, variant, color } = getLogType(message);
|
||||||
|
|
||||||
const formattedTime = timestamp
|
const formattedTime = timestamp
|
||||||
? timestamp.toLocaleString("en-GB", {
|
? timestamp.toLocaleString([], {
|
||||||
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: false,
|
|
||||||
})
|
})
|
||||||
: "--- No time found ---";
|
: "--- No time found ---";
|
||||||
|
|
||||||
@@ -36,7 +41,26 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) {
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
part
|
part
|
||||||
)
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltip = (color: string, timestamp: string) => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||||
|
{timestamp}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,14 +72,14 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) {
|
|||||||
? "bg-red-500/10 hover:bg-red-500/15"
|
? "bg-red-500/10 hover:bg-red-500/15"
|
||||||
: type === "warning"
|
: type === "warning"
|
||||||
? "bg-yellow-500/10 hover:bg-yellow-500/15"
|
? "bg-yellow-500/10 hover:bg-yellow-500/15"
|
||||||
: "hover:bg-gray-200/50 dark:hover:bg-gray-800/50"
|
: "hover:bg-gray-200/50 dark:hover:bg-gray-800/50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
<div className="flex items-start gap-x-2">
|
<div className="flex items-start gap-x-2">
|
||||||
{/* Icon to expand the log item maybe implement a colapsible later */}
|
{/* Icon to expand the log item maybe implement a colapsible later */}
|
||||||
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||||
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} />
|
{rawTimestamp && tooltip(color, rawTimestamp)}
|
||||||
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
|
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
|
||||||
{formattedTime}
|
{formattedTime}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type LogType = "error" | "warning" | "success" | "info";
|
|||||||
export type LogVariant = "red" | "yellow" | "green" | "blue";
|
export type LogVariant = "red" | "yellow" | "green" | "blue";
|
||||||
|
|
||||||
export interface LogLine {
|
export interface LogLine {
|
||||||
|
rawTimestamp: string | null;
|
||||||
timestamp: Date | null;
|
timestamp: Date | null;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -61,11 +62,12 @@ export function parseLogs(logString: string): LogLine[] {
|
|||||||
const cleanedMessage = message
|
const cleanedMessage = message
|
||||||
?.replace(
|
?.replace(
|
||||||
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
|
||||||
""
|
"",
|
||||||
)
|
)
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
rawTimestamp: timestamp,
|
||||||
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
||||||
message: cleanedMessage,
|
message: cleanedMessage,
|
||||||
};
|
};
|
||||||
@@ -107,7 +109,7 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
/(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
|
/(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
|
||||||
lowerMessage
|
lowerMessage,
|
||||||
) ||
|
) ||
|
||||||
/\[(?:success|ok|done)\]/i.test(lowerMessage) ||
|
/\[(?:success|ok|done)\]/i.test(lowerMessage) ||
|
||||||
/(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
|
/(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId
|
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
||||||
containerId={containerId || ""}
|
|
||||||
serverId={serverId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ const badgeVariants = cva(
|
|||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
red:
|
red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
|
|
||||||
yellow:
|
yellow:
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md",
|
"border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md",
|
||||||
orange:
|
orange:
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md",
|
"border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md",
|
||||||
green:
|
green:
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md",
|
"border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md",
|
||||||
blue:
|
blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
|
|
||||||
blank:
|
blank:
|
||||||
"border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md",
|
"border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md",
|
||||||
outline: "text-foreground",
|
outline: "text-foreground",
|
||||||
|
|||||||
Reference in New Issue
Block a user