Merge pull request #2 from drudge/log-test

feat(logs): improvements based on feedback
This commit is contained in:
Nicholas Penree
2024-12-11 19:23:45 -05:00
committed by GitHub
7 changed files with 511 additions and 491 deletions

View File

@@ -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;
@@ -26,11 +26,11 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
const { data } = api.docker.getConfig.useQuery( const { data } = api.docker.getConfig.useQuery(
{ {
containerId, containerId,
serverId, serverId: serverId ?? undefined,
}, },
{ {
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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) ||

View File

@@ -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>

View File

@@ -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",