Skip to content

Commit c6d77ba

Browse files
committed
uptime tracking
1 parent 43be7fc commit c6d77ba

File tree

11 files changed

+785
-85
lines changed

11 files changed

+785
-85
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/website-page-header.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ export function WebsitePageHeader({
9797
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-3">
9898
<div className="flex items-center gap-3">
9999
<div className="flex items-center gap-3">
100-
{showBackButton && (
100+
{showBackButton ? (
101101
<Button asChild size="sm" variant="ghost">
102102
<Link href={`/websites/${websiteId}`}>
103103
<ArrowLeftIcon size={16} />
104104
<span className="xs:inline hidden">Back</span>
105105
</Link>
106106
</Button>
107-
)}
107+
) : null}
108108
<div className="rounded border border-primary/10 bg-primary/5 p-3">
109109
{icon}
110110
</div>
@@ -117,7 +117,7 @@ export function WebsitePageHeader({
117117
</div>
118118

119119
<div className="flex items-center gap-3">
120-
{docsUrl && (
120+
{docsUrl ? (
121121
<Button asChild variant="outline">
122122
<Link
123123
className="cursor-pointer gap-2 transition-all duration-300 hover:border-primary/50 hover:bg-primary/5"
@@ -129,8 +129,8 @@ export function WebsitePageHeader({
129129
<span className="xs:inline hidden">Docs</span>
130130
</Link>
131131
</Button>
132-
)}
133-
{onRefreshAction && (
132+
) : null}
133+
{onRefreshAction ? (
134134
<Button
135135
className="cursor-pointer gap-2 transition-all duration-300 hover:border-primary/50 hover:bg-primary/5"
136136
disabled={isRefreshing}
@@ -143,7 +143,7 @@ export function WebsitePageHeader({
143143
/>
144144
<span className="xs:inline hidden">Refresh</span>
145145
</Button>
146-
)}
146+
) : null}
147147
{additionalActions}
148148
</div>
149149
</div>
@@ -156,14 +156,14 @@ export function WebsitePageHeader({
156156
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
157157
<div className="space-y-2">
158158
<div className="flex items-center gap-3">
159-
{showBackButton && (
159+
{showBackButton ? (
160160
<Button asChild className="mr-2" size="sm" variant="ghost">
161161
<Link href={`/websites/${websiteId}`}>
162162
<ArrowLeftIcon size={16} />
163163
Back
164164
</Link>
165165
</Button>
166-
)}
166+
) : null}
167167
<div className="rounded-lg border border-accent-foreground/10 bg-secondary p-2.5">
168168
{cloneElement(icon, {
169169
...icon.props,
@@ -185,7 +185,7 @@ export function WebsitePageHeader({
185185
</div>
186186
</div>
187187
<div className="flex items-center gap-3">
188-
{docsUrl && (
188+
{docsUrl ? (
189189
<Button asChild variant="outline">
190190
<Link
191191
className="cursor-pointer select-none gap-2 border-border/50"
@@ -197,8 +197,8 @@ export function WebsitePageHeader({
197197
Documentation
198198
</Link>
199199
</Button>
200-
)}
201-
{onRefreshAction && (
200+
) : null}
201+
{onRefreshAction ? (
202202
<Button
203203
disabled={isRefreshing}
204204
onClick={onRefreshAction}
@@ -210,19 +210,19 @@ export function WebsitePageHeader({
210210
/>
211211
Refresh Data
212212
</Button>
213-
)}
214-
{onCreateAction && (
213+
) : null}
214+
{onCreateAction ? (
215215
<Button onClick={onCreateAction}>
216216
<PlusIcon size={16} />
217217
{createActionLabel}
218218
</Button>
219-
)}
219+
) : null}
220220
{additionalActions}
221221
</div>
222222
</div>
223223
</div>
224224

225-
{hasError && (
225+
{hasError ? (
226226
<Card className="rounded border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
227227
<CardContent className="pt-6">
228228
<div className="flex flex-col items-center space-y-3 text-center">
@@ -238,7 +238,7 @@ export function WebsitePageHeader({
238238
`There was an issue loading your ${title.toLowerCase()}. Please try refreshing the page.`}
239239
</p>
240240
</div>
241-
{onRefreshAction && (
241+
{onRefreshAction ? (
242242
<Button
243243
className="cursor-pointer select-none gap-2 rounded transition-all duration-300 hover:border-primary/20 hover:bg-primary/10"
244244
onClick={onRefreshAction}
@@ -248,11 +248,11 @@ export function WebsitePageHeader({
248248
<ArrowClockwiseIcon className="size-4" size={16} />
249249
Retry
250250
</Button>
251-
)}
251+
) : null}
252252
</div>
253253
</CardContent>
254254
</Card>
255-
)}
255+
) : null}
256256
</div>
257257
);
258258
}

apps/dashboard/app/(main)/websites/[id]/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default function WebsiteLayout({ children }: WebsiteLayoutProps) {
4141
"/settings",
4242
"/users",
4343
"/agent",
44+
"/pulse",
4445
];
4546

4647
const isAssistantPage = noToolbarPages.some((page) =>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"use client";
2+
3+
import {
4+
DotsThreeIcon,
5+
HeartbeatIcon,
6+
PencilIcon,
7+
TrashIcon,
8+
} from "@phosphor-icons/react";
9+
import { useMutation } from "@tanstack/react-query";
10+
import { useState } from "react";
11+
import { toast } from "sonner";
12+
import { Button } from "@/components/ui/button";
13+
import { Card, CardContent } from "@/components/ui/card";
14+
import {
15+
DropdownMenu,
16+
DropdownMenuContent,
17+
DropdownMenuItem,
18+
DropdownMenuTrigger,
19+
} from "@/components/ui/dropdown-menu";
20+
import { orpc } from "@/lib/orpc";
21+
22+
const granularityLabels: Record<string, string> = {
23+
minute: "1m",
24+
ten_minutes: "10m",
25+
thirty_minutes: "30m",
26+
hour: "1h",
27+
six_hours: "6h",
28+
twelve_hours: "12h",
29+
day: "Daily",
30+
};
31+
32+
type MonitorCardProps = {
33+
schedule: {
34+
id: string;
35+
granularity: string;
36+
cron: string;
37+
isPaused: boolean;
38+
createdAt: Date | string;
39+
updatedAt: Date | string;
40+
};
41+
onEdit: () => void;
42+
onDelete: () => void;
43+
onRefetch: () => void;
44+
};
45+
46+
export function MonitorCard({
47+
schedule,
48+
onEdit,
49+
onDelete,
50+
onRefetch,
51+
}: MonitorCardProps) {
52+
const [isPausing, setIsPausing] = useState(false);
53+
54+
const pauseMutation = useMutation({
55+
...orpc.uptime.pauseSchedule.mutationOptions(),
56+
});
57+
const resumeMutation = useMutation({
58+
...orpc.uptime.resumeSchedule.mutationOptions(),
59+
});
60+
61+
const handleTogglePause = async () => {
62+
setIsPausing(true);
63+
try {
64+
if (schedule.isPaused) {
65+
await resumeMutation.mutateAsync({ scheduleId: schedule.id });
66+
toast.success("Monitor resumed");
67+
} else {
68+
await pauseMutation.mutateAsync({ scheduleId: schedule.id });
69+
toast.success("Monitor paused");
70+
}
71+
onRefetch();
72+
} catch (error) {
73+
const errorMessage =
74+
error instanceof Error ? error.message : "Failed to update monitor";
75+
toast.error(errorMessage);
76+
} finally {
77+
setIsPausing(false);
78+
}
79+
};
80+
81+
return (
82+
<Card className="rounded">
83+
<CardContent className="p-6">
84+
<div className="flex items-start justify-between">
85+
<div className="flex items-start gap-4">
86+
<div className="flex size-12 items-center justify-center rounded border bg-secondary-brighter">
87+
<HeartbeatIcon
88+
className="text-accent-foreground"
89+
size={24}
90+
weight="duotone"
91+
/>
92+
</div>
93+
<div className="flex-1">
94+
<h3 className="font-semibold text-foreground text-lg">
95+
Uptime Monitor
96+
</h3>
97+
<div className="mt-2 space-y-1 text-muted-foreground text-sm">
98+
<div className="flex items-center gap-2">
99+
<span>Check Frequency:</span>
100+
<span className="font-medium text-foreground">
101+
{granularityLabels[schedule.granularity] ||
102+
schedule.granularity}
103+
</span>
104+
</div>
105+
<div className="flex items-center gap-2">
106+
<span>Status:</span>
107+
<span
108+
className={`font-medium ${schedule.isPaused ? "text-amber-600" : "text-green-600"}`}
109+
>
110+
{schedule.isPaused ? "Paused" : "Active"}
111+
</span>
112+
</div>
113+
</div>
114+
</div>
115+
</div>
116+
117+
<DropdownMenu>
118+
<DropdownMenuTrigger asChild>
119+
<Button size="sm" variant="ghost">
120+
<DotsThreeIcon size={20} weight="duotone" />
121+
</Button>
122+
</DropdownMenuTrigger>
123+
<DropdownMenuContent align="end">
124+
<DropdownMenuItem onClick={onEdit}>
125+
<PencilIcon size={16} />
126+
Edit
127+
</DropdownMenuItem>
128+
<DropdownMenuItem
129+
disabled={
130+
isPausing ||
131+
pauseMutation.isPending ||
132+
resumeMutation.isPending
133+
}
134+
onClick={handleTogglePause}
135+
>
136+
<HeartbeatIcon size={16} />
137+
{schedule.isPaused ? "Resume" : "Pause"}
138+
</DropdownMenuItem>
139+
<DropdownMenuItem
140+
className="text-destructive focus:text-destructive"
141+
onClick={onDelete}
142+
>
143+
<TrashIcon size={16} />
144+
Delete
145+
</DropdownMenuItem>
146+
</DropdownMenuContent>
147+
</DropdownMenu>
148+
</div>
149+
</CardContent>
150+
</Card>
151+
);
152+
}

0 commit comments

Comments
 (0)