Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
Expand All @@ -24,7 +25,6 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";

Expand Down
4 changes: 2 additions & 2 deletions apps/dokploy/components/dashboard/compose/logs/show.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Loader2 } from "lucide-react";
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
import { format } from "date-fns";
import {
ChevronLeft,
ChevronRight,
History,
Info,
Loader2,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";

const PAGE_SIZE = 20;

const RESOURCE_TYPES = [
{ label: "All Resources", value: "all" },
{ label: "Project", value: "project" },
{ label: "Application", value: "application" },
{ label: "Compose", value: "compose" },
{ label: "Database", value: "database" },
{ label: "System", value: "system" },
{ label: "Organization", value: "organization" },
{ label: "Domain", value: "domain" },
];

export const ShowActivityLogs = () => {
const [page, setPage] = useState(1);
const [resourceType, setResourceType] = useState<string>("all");
const utils = api.useUtils();

const { data, isLoading } = api.activityLog.all.useQuery({
page,
pageSize: PAGE_SIZE,
resourceType: resourceType === "all" ? undefined : resourceType,
});

const { mutateAsync: purgeLogs, isLoading: isPurging } =
api.activityLog.purge.useMutation({
onSuccess: (res) => {
toast.success(`Successfully purged ${res.deletedCount} logs.`);
utils.activityLog.all.invalidate();
},
onError: (error) => {
toast.error(error.message || "Failed to purge logs.");
},
});

const handlePurge = async (days: number) => {
const message =
days === 0
? "ALL activity logs?"
: `activity logs older than ${days} days?`;
if (confirm(`Are you sure you want to clear ${message}`)) {
await purgeLogs({
days,
});
}
};

const logs = data?.logs || [];
const totalCount = data?.totalCount || 0;
const totalPages = Math.ceil(totalCount / PAGE_SIZE);

return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="space-y-1">
<CardTitle className="text-xl flex flex-row gap-2">
<History className="size-6 text-muted-foreground self-center" />
Activity Logs
</CardTitle>
<CardDescription>
View all actions performed in your organization.
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select
value={resourceType}
onValueChange={(val) => {
setResourceType(val);
setPage(1);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
{RESOURCE_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
onValueChange={(val) => handlePurge(Number.parseInt(val))}
>
<SelectTrigger className="w-[140px] text-destructive border-destructive/20 hover:bg-destructive/10">
<Trash2 className="size-4 mr-2" />
<SelectValue placeholder="Clear Logs" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Older than 7 days</SelectItem>
<SelectItem value="30">Older than 30 days</SelectItem>
<SelectItem value="90">Older than 90 days</SelectItem>
<SelectItem value="0">Clear All (Careful!)</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-4 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[40vh]">
<span>Loading logs...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{logs.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[40vh] justify-center">
<History className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
No activity logs found.
</span>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader className="bg-muted/50">
<TableRow>
<TableHead>User</TableHead>
<TableHead>Action</TableHead>
<TableHead>Resource</TableHead>
<TableHead className="text-center">Date</TableHead>
<TableHead className="text-right">
Details
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.map((log) => (
<TableRow key={log.activityLogId}>
<TableCell className="max-w-[150px] truncate">
<span className="font-medium text-sm">
{log.user?.email || "System"}
</span>
</TableCell>
<TableCell>
<Badge
variant="outline"
className="capitalize text-[10px] px-1.5 py-0"
>
{log.action.replace(".", " ")}
</Badge>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-[10px] text-muted-foreground uppercase font-bold tracking-wider">
{log.resourceType}
</span>
<span className="text-sm font-medium">
{(log.metadata as any)?.name ||
log.resourceId?.substring(0, 8) ||
"N/A"}
</span>
</div>
</TableCell>
<TableCell className="text-center whitespace-nowrap">
<span className="text-xs text-muted-foreground">
{format(
new Date(log.createdAt),
"MMM d, HH:mm",
)}
</span>
</TableCell>
<TableCell className="text-right">
{log.metadata && (
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8"
>
<Info className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
Activity Details
</DialogTitle>
<DialogDescription>
Detailed information for the logged
action.
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="text-muted-foreground font-medium">
User
</div>
<div>
{log.user?.email || "System"}
</div>
<div className="text-muted-foreground font-medium">
Action
</div>
<div className="capitalize">
{log.action.replace(".", " ")}
</div>
<div className="text-muted-foreground font-medium">
Resource
</div>
<div>
{log.resourceType} (
{log.resourceId?.substring(0, 8)})
</div>
<div className="text-muted-foreground font-medium">
Date
</div>
<div>
{format(
new Date(log.createdAt),
"PPP p",
)}
</div>
</div>

<div className="space-y-2">
<p className="text-xs font-bold uppercase text-muted-foreground border-b pb-1">
Metadata
</p>
<div className="bg-muted/50 p-3 rounded-md text-xs font-mono overflow-auto max-h-[300px]">
{Object.entries(
log.metadata as Record<
string,
any
>,
).map(([key, value]) => (
<div
key={key}
className="flex flex-col mb-2 last:mb-0"
>
<span className="text-blue-500 font-bold">
{key}:
</span>
<span className="pl-2 break-all whitespace-pre-wrap">
{typeof value === "object"
? JSON.stringify(
value,
null,
2,
)
: String(value)}
</span>
</div>
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>

<div className="flex items-center justify-between px-2">
<p className="text-xs text-muted-foreground">
Showing{" "}
<span className="font-medium">
{(page - 1) * PAGE_SIZE + 1}
</span>{" "}
to{" "}
<span className="font-medium">
{Math.min(page * PAGE_SIZE, totalCount)}
</span>{" "}
of <span className="font-medium">{totalCount}</span>{" "}
logs
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="size-8 p-0"
>
<ChevronLeft className="size-4" />
</Button>
<span className="text-xs font-medium">
Page {page} of {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => p + 1)}
disabled={page >= totalPages}
className="size-8 p-0"
>
<ChevronRight className="size-4" />
</Button>
</div>
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormDescription,
FormLabel,
FormMessage,
} from "@/components/ui/form";
Expand Down
Loading