mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(logs): filter improvements
This commit is contained in:
parent
71fe6de9cb
commit
ca4820940e
@ -1,26 +1,16 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectSeparator
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Download as DownloadIcon,
|
||||
Loader2
|
||||
} from "lucide-react";
|
||||
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { SinceLogsFilter } from "./since-logs-filter";
|
||||
import { StatusLogsFilter } from "./status-logs-filter";
|
||||
import { TerminalLine } from "./terminal-line";
|
||||
import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||
import { StatusLogsFilter } from "./status-logs-filter";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||
@ -49,276 +39,264 @@ export const priorities = [
|
||||
];
|
||||
|
||||
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId: serverId ?? undefined,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
}
|
||||
);
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId: serverId ?? undefined,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
},
|
||||
);
|
||||
|
||||
const [rawLogs, setRawLogs] = React.useState("");
|
||||
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const [lines, setLines] = React.useState<number>(100);
|
||||
const [search, setSearch] = React.useState<string>("");
|
||||
const [showTimestamp, setShowTimestamp] = React.useState(true);
|
||||
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||
const [rawLogs, setRawLogs] = React.useState("");
|
||||
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const [lines, setLines] = React.useState<number>(100);
|
||||
const [search, setSearch] = React.useState<string>("");
|
||||
const [showTimestamp, setShowTimestamp] = React.useState(true);
|
||||
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
const scrollToBottom = () => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) return;
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||
setAutoScroll(isAtBottom);
|
||||
};
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||
setAutoScroll(isAtBottom);
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value || "");
|
||||
};
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value || "");
|
||||
};
|
||||
|
||||
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
setLines(Number(e.target.value) || 1);
|
||||
};
|
||||
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
setLines(Number(e.target.value) || 1);
|
||||
};
|
||||
|
||||
const handleSince = (value: TimeFilter) => {
|
||||
if (value === "timestamp") {
|
||||
setShowTimestamp(!showTimestamp);
|
||||
} else {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
setSince(value);
|
||||
}
|
||||
};
|
||||
const handleSince = (value: string) => {
|
||||
if (value === "timestamp") {
|
||||
setShowTimestamp(!showTimestamp);
|
||||
} else {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
setSince(value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerId) return;
|
||||
|
||||
let isCurrentConnection = true;
|
||||
let noDataTimeout: NodeJS.Timeout;
|
||||
setIsLoading(true);
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
useEffect(() => {
|
||||
if (!containerId) return;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const params = new globalThis.URLSearchParams({
|
||||
containerId,
|
||||
tail: lines.toString(),
|
||||
since,
|
||||
search,
|
||||
});
|
||||
let isCurrentConnection = true;
|
||||
let noDataTimeout: NodeJS.Timeout;
|
||||
setIsLoading(true);
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
|
||||
if (serverId) {
|
||||
params.append("serverId", serverId);
|
||||
}
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const params = new globalThis.URLSearchParams({
|
||||
containerId,
|
||||
tail: lines.toString(),
|
||||
since,
|
||||
search,
|
||||
});
|
||||
|
||||
const wsUrl = `${protocol}//${
|
||||
window.location.host
|
||||
}/docker-container-logs?${params.toString()}`;
|
||||
console.log("Connecting to WebSocket:", wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
if (serverId) {
|
||||
params.append("serverId", serverId);
|
||||
}
|
||||
|
||||
const resetNoDataTimeout = () => {
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
noDataTimeout = setTimeout(() => {
|
||||
if (isCurrentConnection) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 2000); // Wait 2 seconds for data before showing "No logs found"
|
||||
};
|
||||
const wsUrl = `${protocol}//${
|
||||
window.location.host
|
||||
}/docker-container-logs?${params.toString()}`;
|
||||
console.log("Connecting to WebSocket:", wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isCurrentConnection) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
console.log("WebSocket connected");
|
||||
resetNoDataTimeout();
|
||||
};
|
||||
const resetNoDataTimeout = () => {
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
noDataTimeout = setTimeout(() => {
|
||||
if (isCurrentConnection) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 2000); // Wait 2 seconds for data before showing "No logs found"
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
if (!isCurrentConnection) return;
|
||||
setRawLogs((prev) => prev + e.data);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
ws.onopen = () => {
|
||||
if (!isCurrentConnection) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
console.log("WebSocket connected");
|
||||
resetNoDataTimeout();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (!isCurrentConnection) return;
|
||||
console.error("WebSocket error:", error);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
if (!isCurrentConnection) return;
|
||||
setRawLogs((prev) => prev + e.data);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
|
||||
ws.onclose = (e) => {
|
||||
if (!isCurrentConnection) return;
|
||||
console.log("WebSocket closed:", e.reason);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
if (!isCurrentConnection) return;
|
||||
console.error("WebSocket error:", error);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
|
||||
return () => {
|
||||
isCurrentConnection = false;
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, [containerId, serverId, lines, search, since]);
|
||||
ws.onclose = (e) => {
|
||||
if (!isCurrentConnection) return;
|
||||
console.log("WebSocket closed:", e.reason);
|
||||
setIsLoading(false);
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const logContent = filteredLogs
|
||||
.map(
|
||||
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||
`${timestamp?.toISOString() || "No timestamp"} ${message}`
|
||||
)
|
||||
.join("\n");
|
||||
return () => {
|
||||
isCurrentConnection = false;
|
||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, [containerId, serverId, lines, search, since]);
|
||||
|
||||
const blob = new Blob([logContent], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
const appName = data.Name.replace("/", "") || "app";
|
||||
const isoDate = new Date().toISOString();
|
||||
a.href = url;
|
||||
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
|
||||
.slice(11, 19)
|
||||
.replace(/:/g, "")}.log.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
const handleDownload = () => {
|
||||
const logContent = filteredLogs
|
||||
.map(
|
||||
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||
`${timestamp?.toISOString() || "No timestamp"} ${message}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const handleFilter = (logs: LogLine[]) => {
|
||||
return logs.filter((log) => {
|
||||
const logType = getLogType(log.message).type;
|
||||
|
||||
if (typeFilter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const blob = new Blob([logContent], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
const appName = data.Name.replace("/", "") || "app";
|
||||
const isoDate = new Date().toISOString();
|
||||
a.href = url;
|
||||
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
|
||||
.slice(11, 19)
|
||||
.replace(/:/g, "")}.log.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return typeFilter.includes(logType);
|
||||
});
|
||||
};
|
||||
const handleFilter = (logs: LogLine[]) => {
|
||||
return logs.filter((log) => {
|
||||
const logType = getLogType(log.message).type;
|
||||
|
||||
useEffect(() => {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
}, [containerId]);
|
||||
if (typeFilter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const logs = parseLogs(rawLogs);
|
||||
const filtered = handleFilter(logs);
|
||||
setFilteredLogs(filtered);
|
||||
}, [rawLogs, search, lines, since, typeFilter]);
|
||||
return typeFilter.includes(logType);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
useEffect(() => {
|
||||
setRawLogs("");
|
||||
setFilteredLogs([]);
|
||||
}, [containerId]);
|
||||
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
useEffect(() => {
|
||||
const logs = parseLogs(rawLogs);
|
||||
const filtered = handleFilter(logs);
|
||||
setFilteredLogs(filtered);
|
||||
}, [rawLogs, search, lines, since, typeFilter]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Number of lines to show"
|
||||
value={lines}
|
||||
onChange={handleLines}
|
||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
/>
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
|
||||
<Select value={since} onValueChange={handleSince}>
|
||||
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
||||
<SelectValue placeholder="Time filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1h">Last hour</SelectItem>
|
||||
<SelectItem value="6h">Last 6 hours</SelectItem>
|
||||
<SelectItem value="24h">Last 24 hours</SelectItem>
|
||||
<SelectItem value="168h">Last 7 days</SelectItem>
|
||||
<SelectItem value="720h">Last 30 days</SelectItem>
|
||||
<SelectItem value="all">All time</SelectItem>
|
||||
<SelectSeparator/>
|
||||
<SelectItem value="timestamp">
|
||||
{showTimestamp ? "Hide timestamp" : "Show timestamp"}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
<StatusLogsFilter
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Number of lines to show"
|
||||
value={lines}
|
||||
onChange={handleLines}
|
||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
/>
|
||||
|
||||
<SinceLogsFilter
|
||||
value={since}
|
||||
onValueChange={handleSince}
|
||||
showTimestamp={showTimestamp}
|
||||
onTimestampChange={setShowTimestamp}
|
||||
/>
|
||||
|
||||
<StatusLogsFilter
|
||||
value={typeFilter}
|
||||
setValue={setTypeFilter}
|
||||
title="Log type"
|
||||
options={priorities}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search logs..."
|
||||
value={search}
|
||||
onChange={handleSearch}
|
||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search logs..."
|
||||
value={search}
|
||||
onChange={handleSearch}
|
||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 sm:w-auto w-full"
|
||||
onClick={handleDownload}
|
||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||
>
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Download logs
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
log={filteredLog}
|
||||
searchTerm={search}
|
||||
noTimestamp={!showTimestamp}
|
||||
/>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
No logs found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 sm:w-auto w-full"
|
||||
onClick={handleDownload}
|
||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||
>
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Download logs
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
log={filteredLog}
|
||||
searchTerm={search}
|
||||
noTimestamp={!showTimestamp}
|
||||
/>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
No logs found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,121 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||
|
||||
const timeRanges = [
|
||||
{
|
||||
label: "All time",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: "Last hour",
|
||||
value: "1h",
|
||||
},
|
||||
{
|
||||
label: "Last 6 hours",
|
||||
value: "6h",
|
||||
},
|
||||
{
|
||||
label: "Last 24 hours",
|
||||
value: "24h",
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: "168h",
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: "720h",
|
||||
},
|
||||
];
|
||||
|
||||
interface SinceLogsFilterProps {
|
||||
value: string;
|
||||
onValueChange: (value: TimeFilter) => void;
|
||||
showTimestamp: boolean;
|
||||
onTimestampChange: (show: boolean) => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function SinceLogsFilter({
|
||||
value,
|
||||
onValueChange,
|
||||
showTimestamp,
|
||||
onTimestampChange,
|
||||
title = "Time range",
|
||||
}: SinceLogsFilterProps) {
|
||||
const selectedLabel =
|
||||
timeRanges.find((range) => range.value === value)?.label ??
|
||||
"Select time range";
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
>
|
||||
{title}
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<div className="space-x-1 flex">
|
||||
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||
{selectedLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{timeRanges.map((range) => {
|
||||
const isSelected = value === range.value;
|
||||
return (
|
||||
<CommandItem
|
||||
key={range.value}
|
||||
onSelect={() => onValueChange(range.value)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<span className="text-sm">{range.label}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<Separator className="my-2" />
|
||||
<div className="p-2 flex items-center justify-between">
|
||||
<span className="text-sm">Show timestamps</span>
|
||||
<Switch checked={showTimestamp} onCheckedChange={onTimestampChange} />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
@ -1,107 +1,170 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
interface StatusLogsFilterProps {
|
||||
value?: string[];
|
||||
setValue?: (value: string[]) => void;
|
||||
title?: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function StatusLogsFilter({
|
||||
value = [],
|
||||
setValue,
|
||||
title,
|
||||
options,
|
||||
}: StatusLogsFilterProps) {
|
||||
const selectedValues = new Set(value as string[]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto">
|
||||
{title}
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<div className="space-x-1 flex">
|
||||
<Badge
|
||||
variant="blank"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selectedValues.size} selected
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value);
|
||||
} else {
|
||||
selectedValues.add(option.value);
|
||||
}
|
||||
const filterValues = Array.from(selectedValues);
|
||||
setValue?.(filterValues.length ? filterValues : []);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Badge variant={
|
||||
option.value === 'success' ? 'green' :
|
||||
option.value === 'error' ? 'red' :
|
||||
option.value === 'warning' ? 'orange' :
|
||||
option.value === 'info' ? 'blue' :
|
||||
option.value === 'debug' ? 'yellow' : 'blank'
|
||||
} >{option.label}</Badge>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
|
||||
interface StatusLogsFilterProps {
|
||||
value?: string[];
|
||||
setValue?: (value: string[]) => void;
|
||||
title?: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function StatusLogsFilter({
|
||||
value = [],
|
||||
setValue,
|
||||
title,
|
||||
options,
|
||||
}: StatusLogsFilterProps) {
|
||||
const selectedValues = new Set(value as string[]);
|
||||
const allSelected = selectedValues.size === 0;
|
||||
|
||||
const getSelectedBadges = () => {
|
||||
if (allSelected) {
|
||||
return (
|
||||
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||
All
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedValues.size >= 1) {
|
||||
const selected = options.find((opt) => selectedValues.has(opt.value));
|
||||
return (
|
||||
<>
|
||||
<Badge
|
||||
variant={
|
||||
selected?.value === "success"
|
||||
? "green"
|
||||
: selected?.value === "error"
|
||||
? "red"
|
||||
: selected?.value === "warning"
|
||||
? "orange"
|
||||
: selected?.value === "info"
|
||||
? "blue"
|
||||
: selected?.value === "debug"
|
||||
? "yellow"
|
||||
: "blank"
|
||||
}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selected?.label}
|
||||
</Badge>
|
||||
{selectedValues.size > 1 && (
|
||||
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||
+{selectedValues.size - 1}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||
>
|
||||
{title}
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<div className="space-x-1 flex">{getSelectedBadges()}</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setValue?.([]); // Empty array means "All"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||
allSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<Badge variant="blank">All</Badge>
|
||||
</CommandItem>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
const newValues = new Set(selectedValues);
|
||||
if (isSelected) {
|
||||
newValues.delete(option.value);
|
||||
} else {
|
||||
newValues.add(option.value);
|
||||
}
|
||||
setValue?.(Array.from(newValues));
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Badge
|
||||
variant={
|
||||
option.value === "success"
|
||||
? "green"
|
||||
: option.value === "error"
|
||||
? "red"
|
||||
: option.value === "warning"
|
||||
? "orange"
|
||||
: option.value === "info"
|
||||
? "blue"
|
||||
: option.value === "debug"
|
||||
? "yellow"
|
||||
: "blank"
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export type LogType = "error" | "warning" | "success" | "info" | "debug";
|
||||
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
|
||||
export type LogType = "error" | "warning" | "success" | "info" | "debug";
|
||||
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
|
||||
|
||||
export interface LogLine {
|
||||
rawTimestamp: string | null;
|
||||
@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => {
|
||||
|
||||
if (
|
||||
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
||||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) ||
|
||||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage)
|
||||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
|
||||
lowerMessage,
|
||||
) ||
|
||||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
|
||||
lowerMessage,
|
||||
)
|
||||
) {
|
||||
return LOG_STYLES.debug;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user