Skip to content

Commit 9357d8c

Browse files
Implement phase 0 monitor history groundwork
1 parent 60ad003 commit 9357d8c

8 files changed

Lines changed: 196 additions & 16 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
4747

4848
UPTIME_MONITOR_MAIL_TO='' # comma separated string of emails
4949
UPTIME_MONITOR_GOOGLE_CHAT_WEBHOOK_URL=''
50+
MONITOR_HISTORY_ENABLED=false

app/Http/Middleware/HandleInertiaRequests.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public function share(Request $request)
3535
'auth' => [
3636
'user' => $request->user(),
3737
],
38+
'features' => [
39+
'monitorHistory' => config('monitor-history.enabled'),
40+
],
3841
]);
3942
}
4043
}

config/monitor-history.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
return [
4+
5+
/*
6+
* Toggle for monitor history features.
7+
*/
8+
'enabled' => (bool) env('MONITOR_HISTORY_ENABLED', false),
9+
];

resources/js/Components/MonitorCard.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import MonitorCheckIntervalIcon from "./MonitorCheckIntervalIcon";
55
import { Link, router } from "@inertiajs/react";
66
import {
77
ArrowTopRightOnSquareIcon,
8+
EyeIcon,
89
PencilIcon,
910
TrashIcon,
1011
} from "@heroicons/react/24/outline";
@@ -44,6 +45,13 @@ export default function MonitorCard({ monitor }) {
4445
</div>
4546
</div>
4647
<div className="flex gap-1 ml-3 shrink-0">
48+
<Link
49+
href={route("monitors.show", monitor.id)}
50+
className="p-2 rounded-lg text-gray-400 hover:bg-gray-50 hover:text-gray-700 transition-colors"
51+
title="View monitor details"
52+
>
53+
<EyeIcon className="h-4 w-4" />
54+
</Link>
4755
<Link
4856
href={route("monitors.edit", monitor.id)}
4957
className="p-2 rounded-lg text-gray-400 hover:bg-purple-50 hover:text-purple-600 transition-colors"

resources/js/Components/MonitorDomainIcon.jsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import React from 'react';
22
import { GlobeAltIcon, NoSymbolIcon } from '@heroicons/react/24/solid';
33
import Badge from './Badge';
4+
import {
5+
CHECK_STATUS,
6+
getCheckStatusBadgeColor,
7+
} from '@/Utils/checkStatusSeverity';
48

59
export default function MonitorDomainIcon({ monitor }) {
610

711
let badgeProps = { icon: null, text: '', color: '' };
812

913
if (! monitor.domain_expires_at) {
1014
badgeProps = {
11-
icon: <NoSymbolIcon className="h-4 w-4 text-red-600" />,
15+
icon: <NoSymbolIcon className="h-4 w-4 text-gray-600" />,
1216
text: 'No Data',
13-
color: 'red'
17+
color: getCheckStatusBadgeColor(CHECK_STATUS.UNKNOWN),
1418
};
1519
} else {
1620
const today = new Date();
@@ -21,42 +25,42 @@ export default function MonitorDomainIcon({ monitor }) {
2125
badgeProps = {
2226
icon: <GlobeAltIcon className="h-4 w-4 text-green-600" />,
2327
text: '100+ days left',
24-
color: 'green'
28+
color: getCheckStatusBadgeColor(CHECK_STATUS.SUCCESS),
2529
};
2630
break;
2731
case daysLeft <= 100 && daysLeft > 30:
2832
badgeProps = {
2933
icon: <GlobeAltIcon className="h-4 w-4 text-blue-600" />,
3034
text: `${daysLeft} days left`,
31-
color: 'blue'
35+
color: getCheckStatusBadgeColor(CHECK_STATUS.SUCCESS),
3236
};
3337
break;
3438
case daysLeft <= 30 && daysLeft > 7:
3539
badgeProps = {
36-
icon: <GlobeAltIcon className="h-4 w-4 text-purple-600" />,
40+
icon: <GlobeAltIcon className="h-4 w-4 text-yellow-600" />,
3741
text: `${daysLeft} days left`,
38-
color: 'purple'
42+
color: getCheckStatusBadgeColor(CHECK_STATUS.WARNING),
3943
};
4044
break;
4145
case daysLeft <= 7 && daysLeft > 1:
4246
badgeProps = {
4347
icon: <GlobeAltIcon className="h-4 w-4 text-yellow-600" />,
4448
text: `${daysLeft} days left`,
45-
color: 'yellow'
49+
color: getCheckStatusBadgeColor(CHECK_STATUS.WARNING),
4650
};
4751
break;
4852
case daysLeft === 1:
4953
badgeProps = {
50-
icon: <GlobeAltIcon className="h-4 w-4 text-pink-600" />,
54+
icon: <GlobeAltIcon className="h-4 w-4 text-yellow-600" />,
5155
text: `${daysLeft} day left`,
52-
color: 'pink'
56+
color: getCheckStatusBadgeColor(CHECK_STATUS.WARNING),
5357
};
5458
break;
5559
case daysLeft <= 0:
5660
badgeProps = {
5761
icon: <GlobeAltIcon className="h-4 w-4 text-red-600" />,
5862
text: 'Domain Expired',
59-
color: 'red'
63+
color: getCheckStatusBadgeColor(CHECK_STATUS.FAILED),
6064
};
6165
break;
6266
default:
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import React from 'react';
22
import { CheckBadgeIcon, ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
33
import Badge from './Badge';
4+
import {
5+
CHECK_STATUS,
6+
getCheckStatusBadgeColor,
7+
mapUptimeStatusToCheckStatus,
8+
} from '@/Utils/checkStatusSeverity';
49

510
export default function MonitorUptimeIcon ({ monitor }) {
6-
if (monitor.uptime_status == 'up') {
7-
return <Badge icon={<CheckBadgeIcon className="h-4 w-4 text-green-600"/>} text="UP" color="green" />;
11+
const checkStatus = mapUptimeStatusToCheckStatus(monitor.uptime_status);
12+
13+
if (checkStatus === CHECK_STATUS.SUCCESS) {
14+
return <Badge icon={<CheckBadgeIcon className="h-4 w-4 text-green-600"/>} text="UP" color={getCheckStatusBadgeColor(checkStatus)} />;
815
}
9-
if (monitor.uptime_status == 'down') {
10-
return <Badge icon={<XCircleIcon className="h-4 w-4 text-red-600"/>} text="DOWN" color="red" />;
16+
if (checkStatus === CHECK_STATUS.FAILED) {
17+
return <Badge icon={<XCircleIcon className="h-4 w-4 text-red-600"/>} text="DOWN" color={getCheckStatusBadgeColor(checkStatus)} />;
1118
}
12-
if (monitor.uptime_status == 'not yet checked') {
13-
return <Badge icon={<ExclamationCircleIcon className="h-4 w-4 text-blue-600"/>} text="PENDING" color="blue" />;
19+
if (checkStatus === CHECK_STATUS.UNKNOWN) {
20+
return <Badge icon={<ExclamationCircleIcon className="h-4 w-4 text-gray-600"/>} text="PENDING" color={getCheckStatusBadgeColor(checkStatus)} />;
1421
}
1522
return <></>;
1623
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from "react";
2+
import Authenticated from "@/Layouts/Authenticated";
3+
import { Head, Link, usePage } from "@inertiajs/react";
4+
import {
5+
ArrowTopRightOnSquareIcon,
6+
ArrowLeftIcon,
7+
} from "@heroicons/react/24/outline";
8+
import MonitorUptimeIcon from "@/Components/MonitorUptimeIcon";
9+
import MonitorDomainIcon from "@/Components/MonitorDomainIcon";
10+
import MonitorCheckIntervalIcon from "@/Components/MonitorCheckIntervalIcon";
11+
import PageHeader from "@/Components/PageHeader";
12+
13+
export default function Show(props) {
14+
const { monitor, features } = usePage().props;
15+
const isHistoryEnabled = Boolean(features?.monitorHistory);
16+
17+
return (
18+
<Authenticated auth={props.auth} errors={props.errors}>
19+
<Head title={monitor.name || "Monitor Details"} />
20+
21+
<PageHeader>
22+
<div className="flex items-center justify-between gap-4">
23+
<div>
24+
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
25+
{monitor.name}
26+
</h1>
27+
<div className="mt-1 flex items-center gap-1 text-sm text-gray-500">
28+
<span className="truncate max-w-[35rem]">
29+
{monitor.raw_url}
30+
</span>
31+
<a
32+
href={monitor.raw_url}
33+
target="_blank"
34+
rel="noreferrer"
35+
className="text-gray-400 hover:text-gray-600"
36+
title="Open monitor URL"
37+
>
38+
<ArrowTopRightOnSquareIcon className="h-4 w-4" />
39+
</a>
40+
</div>
41+
</div>
42+
<Link
43+
href={route("monitors.index")}
44+
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
45+
>
46+
<ArrowLeftIcon className="h-4 w-4" />
47+
Back
48+
</Link>
49+
</div>
50+
</PageHeader>
51+
52+
<div className="max-w-7xl mx-auto py-8 px-6 lg:px-8 space-y-6">
53+
<div className="bg-white rounded-xl p-6 border border-gray-200 shadow-sm">
54+
<h2 className="text-sm font-semibold tracking-wide text-gray-500 uppercase mb-4">
55+
Monitor Snapshot
56+
</h2>
57+
<div className="flex items-center gap-2.5 flex-wrap">
58+
<MonitorUptimeIcon monitor={monitor} />
59+
<MonitorCheckIntervalIcon monitor={monitor} />
60+
<MonitorDomainIcon monitor={monitor} />
61+
</div>
62+
</div>
63+
64+
<div className="bg-white rounded-xl p-6 border border-gray-200 shadow-sm">
65+
<h2 className="text-sm font-semibold tracking-wide text-gray-500 uppercase mb-2">
66+
Monitor History
67+
</h2>
68+
{isHistoryEnabled ? (
69+
<p className="text-sm text-gray-600">
70+
History view is enabled. Daily metrics and check logs
71+
will be available in the next implementation phase.
72+
</p>
73+
) : (
74+
<p className="text-sm text-gray-600">
75+
History view is disabled. Set{" "}
76+
<code>MONITOR_HISTORY_ENABLED=true</code> to enable
77+
rollout when backend history ingestion is ready.
78+
</p>
79+
)}
80+
</div>
81+
</div>
82+
</Authenticated>
83+
);
84+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export const CHECK_STATUS = Object.freeze({
2+
SUCCESS: "success",
3+
WARNING: "warning",
4+
FAILED: "failed",
5+
UNKNOWN: "unknown",
6+
});
7+
8+
const CHECK_STATUS_META = Object.freeze({
9+
[CHECK_STATUS.SUCCESS]: {
10+
severity: 0,
11+
badgeColor: "green",
12+
heatmapClass: "bg-green-500",
13+
label: "Healthy",
14+
},
15+
[CHECK_STATUS.WARNING]: {
16+
severity: 1,
17+
badgeColor: "yellow",
18+
heatmapClass: "bg-yellow-400",
19+
label: "Warning",
20+
},
21+
[CHECK_STATUS.FAILED]: {
22+
severity: 2,
23+
badgeColor: "red",
24+
heatmapClass: "bg-red-500",
25+
label: "Failed",
26+
},
27+
[CHECK_STATUS.UNKNOWN]: {
28+
severity: 3,
29+
badgeColor: "gray",
30+
heatmapClass: "bg-gray-300",
31+
label: "Unknown",
32+
},
33+
});
34+
35+
export function normalizeCheckStatus(status) {
36+
if (Object.values(CHECK_STATUS).includes(status)) {
37+
return status;
38+
}
39+
40+
return CHECK_STATUS.UNKNOWN;
41+
}
42+
43+
export function getCheckStatusMeta(status) {
44+
const normalizedStatus = normalizeCheckStatus(status);
45+
46+
return CHECK_STATUS_META[normalizedStatus];
47+
}
48+
49+
export function getCheckStatusBadgeColor(status) {
50+
return getCheckStatusMeta(status).badgeColor;
51+
}
52+
53+
export function mapUptimeStatusToCheckStatus(uptimeStatus) {
54+
switch (uptimeStatus) {
55+
case "up":
56+
return CHECK_STATUS.SUCCESS;
57+
case "down":
58+
return CHECK_STATUS.FAILED;
59+
case "not yet checked":
60+
return CHECK_STATUS.UNKNOWN;
61+
default:
62+
return CHECK_STATUS.UNKNOWN;
63+
}
64+
}

0 commit comments

Comments
 (0)