feat(logs): filter improvements

This commit is contained in:
Nicholas Penree
2024-12-16 16:51:29 -05:00
parent 71fe6de9cb
commit ca4820940e
4 changed files with 529 additions and 363 deletions

View File

@@ -1,22 +1,12 @@
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;
@@ -56,7 +46,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
},
{
enabled: !!containerId,
}
},
);
const [rawLogs, setRawLogs] = React.useState("");
@@ -94,7 +84,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
setLines(Number(e.target.value) || 1);
};
const handleSince = (value: TimeFilter) => {
const handleSince = (value: string) => {
if (value === "timestamp") {
setShowTimestamp(!showTimestamp);
} else {
@@ -183,7 +173,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
const logContent = filteredLogs
.map(
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
`${timestamp?.toISOString() || "No timestamp"} ${message}`
`${timestamp?.toISOString() || "No timestamp"} ${message}`,
)
.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"
/>
<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>
<SinceLogsFilter
value={since}
onValueChange={handleSince}
showTimestamp={showTimestamp}
onTimestampChange={setShowTimestamp}
/>
<StatusLogsFilter
value={typeFilter}
@@ -279,7 +258,6 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
onChange={handleSearch}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
</div>
<Button

View File

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

View File

@@ -2,9 +2,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
@@ -16,7 +14,7 @@ import {
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { CheckIcon } from "lucide-react";
import React from "react";
import type React from "react";
interface StatusLogsFilterProps {
value?: string[];
@@ -36,42 +34,98 @@ export function StatusLogsFilter({
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">
<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>
<div className="space-x-1 flex">{getSelectedBadges()}</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<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) {
selectedValues.delete(option.value);
newValues.delete(option.value);
} else {
selectedValues.add(option.value);
newValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
setValue?.(filterValues.length ? filterValues : []);
setValue?.(Array.from(newValues));
}}
>
<div
@@ -87,18 +141,27 @@ export function StatusLogsFilter({
{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>
<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>

View File

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