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:
@@ -1,22 +1,12 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectSeparator
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
||||||
Download as DownloadIcon,
|
|
||||||
Loader2
|
|
||||||
} from "lucide-react";
|
|
||||||
import React, { useEffect, useRef } from "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 { TerminalLine } from "./terminal-line";
|
||||||
import { type LogLine, getLogType, parseLogs } from "./utils";
|
import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||||
import { StatusLogsFilter } from "./status-logs-filter";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
@@ -56,7 +46,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: !!containerId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [rawLogs, setRawLogs] = React.useState("");
|
const [rawLogs, setRawLogs] = React.useState("");
|
||||||
@@ -94,7 +84,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
setLines(Number(e.target.value) || 1);
|
setLines(Number(e.target.value) || 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSince = (value: TimeFilter) => {
|
const handleSince = (value: string) => {
|
||||||
if (value === "timestamp") {
|
if (value === "timestamp") {
|
||||||
setShowTimestamp(!showTimestamp);
|
setShowTimestamp(!showTimestamp);
|
||||||
} else {
|
} else {
|
||||||
@@ -183,7 +173,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");
|
||||||
|
|
||||||
@@ -247,23 +237,12 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select value={since} onValueChange={handleSince}>
|
<SinceLogsFilter
|
||||||
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
value={since}
|
||||||
<SelectValue placeholder="Time filter" />
|
onValueChange={handleSince}
|
||||||
</SelectTrigger>
|
showTimestamp={showTimestamp}
|
||||||
<SelectContent>
|
onTimestampChange={setShowTimestamp}
|
||||||
<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>
|
|
||||||
|
|
||||||
<StatusLogsFilter
|
<StatusLogsFilter
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
@@ -279,7 +258,6 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
@@ -16,7 +14,7 @@ import {
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
interface StatusLogsFilterProps {
|
interface StatusLogsFilterProps {
|
||||||
value?: string[];
|
value?: string[];
|
||||||
@@ -36,42 +34,98 @@ export function StatusLogsFilter({
|
|||||||
options,
|
options,
|
||||||
}: StatusLogsFilterProps) {
|
}: StatusLogsFilterProps) {
|
||||||
const selectedValues = new Set(value as string[]);
|
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 (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||||
<div className="space-x-1 flex">
|
<div className="space-x-1 flex">{getSelectedBadges()}</div>
|
||||||
<Badge
|
|
||||||
variant="blank"
|
|
||||||
className="rounded-sm px-1 font-normal"
|
|
||||||
>
|
|
||||||
{selectedValues.size} selected
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[200px] p-0" align="start">
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder={title} />
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
<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) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.has(option.value);
|
const isSelected = selectedValues.has(option.value);
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
const newValues = new Set(selectedValues);
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedValues.delete(option.value);
|
newValues.delete(option.value);
|
||||||
} else {
|
} else {
|
||||||
selectedValues.add(option.value);
|
newValues.add(option.value);
|
||||||
}
|
}
|
||||||
const filterValues = Array.from(selectedValues);
|
setValue?.(Array.from(newValues));
|
||||||
setValue?.(filterValues.length ? filterValues : []);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -87,18 +141,27 @@ export function StatusLogsFilter({
|
|||||||
{option.icon && (
|
{option.icon && (
|
||||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<Badge variant={
|
<Badge
|
||||||
option.value === 'success' ? 'green' :
|
variant={
|
||||||
option.value === 'error' ? 'red' :
|
option.value === "success"
|
||||||
option.value === 'warning' ? 'orange' :
|
? "green"
|
||||||
option.value === 'info' ? 'blue' :
|
: option.value === "error"
|
||||||
option.value === 'debug' ? 'yellow' : 'blank'
|
? "red"
|
||||||
} >{option.label}</Badge>
|
: option.value === "warning"
|
||||||
|
? "orange"
|
||||||
|
: option.value === "info"
|
||||||
|
? "blue"
|
||||||
|
: option.value === "debug"
|
||||||
|
? "yellow"
|
||||||
|
: "blank"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Badge>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
||||||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) ||
|
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
|
||||||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage)
|
lowerMessage,
|
||||||
|
) ||
|
||||||
|
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
|
||||||
|
lowerMessage,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return LOG_STYLES.debug;
|
return LOG_STYLES.debug;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user