diff --git a/application/src/components/services/ResponseTimeChart.tsx b/application/src/components/services/ResponseTimeChart.tsx
index affb107..1383863 100644
--- a/application/src/components/services/ResponseTimeChart.tsx
+++ b/application/src/components/services/ResponseTimeChart.tsx
@@ -37,10 +37,31 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
date: format(timestamp, 'MMM dd, yyyy'),
value: data.status === "paused" ? null : data.responseTime,
status: data.status,
+ // Separate values for different statuses with proper positioning
+ upValue: data.status === "up" ? data.responseTime : null,
+ downValue: data.status === "down" ? data.responseTime : null,
+ warningValue: data.status === "warning" ? data.responseTime : null,
};
});
}, [uptimeData]);
+ // Calculate Y-axis domain for better positioning
+ const yAxisDomain = useMemo(() => {
+ if (!chartData.length) return ['dataMin - 10', 'dataMax + 10'];
+
+ const allValues = chartData
+ .filter(d => d.value !== null && d.status !== 'paused')
+ .map(d => d.value);
+
+ if (allValues.length === 0) return [0, 100];
+
+ const minValue = Math.min(...allValues);
+ const maxValue = Math.max(...allValues);
+ const padding = (maxValue - minValue) * 0.1 || 10;
+
+ return [Math.max(0, minValue - padding), maxValue + padding];
+ }, [chartData]);
+
// Create a custom tooltip for the chart
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
@@ -79,31 +100,6 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
return null;
};
- // Compute status segments for different areas
- const getStatusSegments = () => {
- const segments = {
- up: [] as any[],
- down: [] as any[],
- warning: [] as any[]
- };
-
- chartData.forEach(point => {
- if (point.status === "paused") return;
-
- if (point.status === "up") {
- segments.up.push(point);
- } else if (point.status === "down") {
- segments.down.push(point);
- } else if (point.status === "warning") {
- segments.warning.push(point);
- }
- });
-
- return segments;
- };
-
- const segments = getStatusSegments();
-
// Check if we have any data to display - be more lenient by checking raw uptimeData
const hasData = uptimeData.length > 0;
@@ -172,46 +168,40 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
} />
- {/* Area charts for different statuses */}
- {segments.up.length > 0 && (
-
- )}
+ {/* Separate area charts for each status - positioned closer together */}
+
- {segments.down.length > 0 && (
-
- )}
+
- {segments.warning.length > 0 && (
-
- )}
+
{/* Add reference lines for paused periods */}
{chartData.map((entry, index) =>
@@ -231,4 +221,4 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
);
-}
+}
\ No newline at end of file
diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
index 20f0687..1d096df 100644
--- a/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
+++ b/application/src/components/services/ServiceDetailContainer/hooks/useRealTimeUpdates.tsx
@@ -55,7 +55,8 @@ export const useRealTimeUpdates = ({
setUptimeData(prev => {
const newData: UptimeData = {
id: e.record.id,
- serviceId: e.record.service_id,
+ service_id: e.record.service_id, // Include service_id
+ serviceId: e.record.service_id, // Keep for backward compatibility
timestamp: e.record.timestamp,
status: e.record.status,
responseTime: e.record.response_time || 0,
@@ -84,4 +85,4 @@ export const useRealTimeUpdates = ({
console.error("Error setting up real-time updates:", error);
}
}, [serviceId, startDate, endDate, setService, setUptimeData]);
-};
+};
\ No newline at end of file
diff --git a/application/src/components/services/hooks/useUptimeData.ts b/application/src/components/services/hooks/useUptimeData.ts
index 91e5391..34f7b88 100644
--- a/application/src/components/services/hooks/useUptimeData.ts
+++ b/application/src/components/services/hooks/useUptimeData.ts
@@ -68,7 +68,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
id: `placeholder-${serviceId}-${index}`,
- serviceId: serviceId || "",
+ service_id: serviceId || "", // Include service_id
+ serviceId: serviceId || "", // Keep for backward compatibility
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
status: statusValue as "up" | "down" | "warning" | "paused",
responseTime: 0
@@ -96,7 +97,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
return {
id: `padding-${serviceId}-${index}`,
- serviceId: serviceId || "",
+ service_id: serviceId || "", // Include service_id
+ serviceId: serviceId || "", // Keep for backward compatibility
timestamp: new Date(baseTime - timeOffset).toISOString(),
status: lastStatus,
responseTime: 0
diff --git a/application/src/components/services/incident-history/IncidentTable.tsx b/application/src/components/services/incident-history/IncidentTable.tsx
index 5f7c125..d8118ed 100644
--- a/application/src/components/services/incident-history/IncidentTable.tsx
+++ b/application/src/components/services/incident-history/IncidentTable.tsx
@@ -25,6 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
{t("time")}
{t("status")}
{t("responseTime")}
+ Error Message
+ Details
@@ -52,10 +54,28 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
? `${check.responseTime}ms`
: "N/A"}
+
+ {check.error_message ? (
+
+ {check.error_message}
+
+ ) : (
+ -
+ )}
+
+
+ {check.details ? (
+
+ {check.details}
+
+ ) : (
+ -
+ )}
+
);
})}
);
-}
+}
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
index 8314d36..3cb0a5e 100644
--- a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
+++ b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
@@ -1,3 +1,4 @@
+
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -18,7 +19,8 @@ const formSchema = z.object({
domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().int().min(1).max(365),
expiry_threshold: z.coerce.number().int().min(1).max(30),
- notification_channel: z.string().min(1, "Notification channel is required")
+ notification_channel: z.string().min(1, "Notification channel is required"),
+ check_interval: z.coerce.number().int().min(1).max(30).optional()
});
interface AddSSLCertificateFormProps {
@@ -42,7 +44,8 @@ export const AddSSLCertificateForm = ({
domain: "",
warning_threshold: 30,
expiry_threshold: 7,
- notification_channel: ""
+ notification_channel: "",
+ check_interval: 1
}
});
@@ -89,7 +92,8 @@ export const AddSSLCertificateForm = ({
domain: values.domain,
warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold,
- notification_channel: values.notification_channel
+ notification_channel: values.notification_channel,
+ check_interval: values.check_interval
};
await onSubmit(certData);
@@ -117,34 +121,53 @@ export const AddSSLCertificateForm = ({
)}
/>
+
+ (
+
+ {t('warningThreshold')}
+
+
+
+
+ {t('getNotifiedExpiration')}
+
+
+
+ )}
+ />
+
+ (
+
+ {t('expiryThreshold')}
+
+
+
+
+ {t('getNotifiedCritical')}
+
+
+
+ )}
+ />
+
+
(
-
- {t('warningThreshold')}
-
-
-
-
- {t('getNotifiedExpiration')}
-
-
-
- )}
- />
-
- (
- {t('expiryThreshold')}
+ Check Interval (Days)
-
+
- {t('getNotifiedCritical')}
+ How often to check the SSL certificate (in days)
diff --git a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
index 1512ce4..0e1cad1 100644
--- a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
+++ b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
@@ -1,3 +1,4 @@
+
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -17,6 +18,7 @@ const formSchema = z.object({
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
notification_channel: z.string().min(1, "Notification channel is required"),
+ check_interval: z.coerce.number().int().min(1).max(30).optional(),
});
type FormValues = z.infer;
@@ -40,6 +42,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold,
notification_channel: certificate.notification_channel,
+ check_interval: certificate.check_interval || 1,
},
});
@@ -78,7 +81,8 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
...data,
// Ensure values are correctly typed as numbers
warning_threshold: Number(data.warning_threshold),
- expiry_threshold: Number(data.expiry_threshold)
+ expiry_threshold: Number(data.expiry_threshold),
+ check_interval: data.check_interval ? Number(data.check_interval) : undefined
};
console.log("Submitting updated certificate:", updatedCertificate);
@@ -113,7 +117,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
)}
/>
-
+
)}
/>
+
+ (
+
+ Check Interval (Days)
+
+
+
+
+ How often to check
+
+
+
+ )}
+ />
);
-};
+};
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/SSLCertificateActions.tsx b/application/src/components/ssl-domain/SSLCertificateActions.tsx
new file mode 100644
index 0000000..a1a2148
--- /dev/null
+++ b/application/src/components/ssl-domain/SSLCertificateActions.tsx
@@ -0,0 +1,70 @@
+
+import React from "react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+import { MoreHorizontal, RefreshCw, Edit, Trash2, Eye } from "lucide-react";
+import { SSLCertificate } from "@/types/ssl.types";
+import { triggerImmediateCheck } from "@/services/sslCertificateService";
+import { toast } from "sonner";
+import { useLanguage } from "@/contexts/LanguageContext";
+
+interface SSLCertificateActionsProps {
+ certificate: SSLCertificate;
+ onView: (certificate: SSLCertificate) => void;
+ onEdit: (certificate: SSLCertificate) => void;
+ onDelete: (certificate: SSLCertificate) => void;
+}
+
+export const SSLCertificateActions = ({
+ certificate,
+ onView,
+ onEdit,
+ onDelete
+}: SSLCertificateActionsProps) => {
+ const { t } = useLanguage();
+
+ const handleCheck = async () => {
+ try {
+ await triggerImmediateCheck(certificate.id);
+ } catch (error) {
+ console.error("Error triggering SSL check:", error);
+ }
+ };
+
+ return (
+
+
+
+
+
+ onView(certificate)}>
+
+ {t('view')}
+
+
+
+ Check
+
+ onEdit(certificate)}>
+
+ {t('edit')}
+
+ onDelete(certificate)}
+ className="text-destructive"
+ >
+
+ {t('delete')}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/SSLCertificateDetailDialog.tsx b/application/src/components/ssl-domain/SSLCertificateDetailDialog.tsx
new file mode 100644
index 0000000..df23233
--- /dev/null
+++ b/application/src/components/ssl-domain/SSLCertificateDetailDialog.tsx
@@ -0,0 +1,219 @@
+
+import React from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import { SSLCertificate } from "@/types/ssl.types";
+import { SSLStatusBadge } from "./SSLStatusBadge";
+import { useLanguage } from "@/contexts/LanguageContext";
+
+interface SSLCertificateDetailDialogProps {
+ certificate: SSLCertificate | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export const SSLCertificateDetailDialog = ({
+ certificate,
+ open,
+ onOpenChange,
+}: SSLCertificateDetailDialogProps) => {
+ const { t } = useLanguage();
+
+ if (!certificate) return null;
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
index 43ec130..ce6fb15 100644
--- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx
+++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
@@ -1,362 +1,255 @@
import React, { useState } from "react";
-import { format } from "date-fns";
-import {
- Table,
- TableHeader,
- TableRow,
- TableHead,
- TableBody,
- TableCell
-} from "@/components/ui/table";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
-import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
-import { SSLCertificate } from "@/types/ssl.types";
-import { SSLStatusBadge } from "./SSLStatusBadge";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { toast } from "sonner";
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { SSLStatusBadge } from "./SSLStatusBadge";
+import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
+import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
+import { SSLCertificateActions } from "./SSLCertificateActions";
+import { SSLCertificateDetailDialog } from "./SSLCertificateDetailDialog";
+import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
+import { pb } from "@/lib/pocketbase";
+import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext";
+import { toast } from "sonner";
-interface SSLCertificatesTableProps {
- certificates: SSLCertificate[];
- onRefresh: (id: string) => void;
- refreshingId: string | null;
- onEdit?: (certificate: SSLCertificate) => void;
- onDelete?: (certificate: SSLCertificate) => void;
-}
-
-export const SSLCertificatesTable = ({
- certificates,
- onRefresh,
- refreshingId,
- onEdit,
- onDelete
-}: SSLCertificatesTableProps) => {
+export const SSLCertificatesTable = () => {
const { t } = useLanguage();
- const [selectedCert, setSelectedCert] = useState(null);
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
- const [certToDelete, setCertToDelete] = useState(null);
-
- const formatDate = (dateString: string | undefined) => {
- if (!dateString) return t('unknown');
-
+ const queryClient = useQueryClient();
+ const [showAddDialog, setShowAddDialog] = useState(false);
+ const [showEditDialog, setShowEditDialog] = useState(false);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const [showViewDialog, setShowViewDialog] = useState(false);
+ const [selectedCertificate, setSelectedCertificate] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { data: certificates = [], isLoading, isError } = useQuery({
+ queryKey: ['ssl-certificates'],
+ queryFn: fetchSSLCertificates,
+ });
+
+ if (isLoading) return Loading...
;
+ if (isError) return Error loading certificates
;
+
+ const handleAddCertificate = async (data: any) => {
+ setIsSubmitting(true);
try {
- const date = new Date(dateString);
- if (isNaN(date.getTime())) {
- console.warn("Invalid date for formatting:", dateString);
- return t('unknown');
- }
- return format(date, "MMM dd, yyyy");
+ await addSSLCertificate(data);
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowAddDialog(false);
+ toast.success(t('certificateAdded'));
} catch (error) {
- console.error("Error formatting date:", error);
- return t('unknown');
+ console.error("Error adding certificate:", error);
+ toast.error(t('failedToAddCertificate'));
+ } finally {
+ setIsSubmitting(false);
}
};
- const handleViewCertificate = (certificate: SSLCertificate) => {
- setSelectedCert(certificate);
+ const handleEditCertificate = async (updatedCertificate: SSLCertificate) => {
+ setIsSubmitting(true);
+ try {
+ await pb.collection('ssl_certificates').update(updatedCertificate.id, {
+ warning_threshold: updatedCertificate.warning_threshold,
+ expiry_threshold: updatedCertificate.expiry_threshold,
+ notification_channel: updatedCertificate.notification_channel,
+ check_interval: updatedCertificate.check_interval,
+ });
+
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowEditDialog(false);
+ setSelectedCertificate(null);
+ toast.success(t('certificateUpdated'));
+ } catch (error) {
+ console.error("Error updating certificate:", error);
+ toast.error(t('failedToUpdateCertificate'));
+ } finally {
+ setIsSubmitting(false);
+ }
};
- const handleEditCertificate = (certificate: SSLCertificate) => {
- if (onEdit) {
- onEdit(certificate);
- } else {
- toast.error("Edit functionality not implemented yet");
+ const handleDeleteCertificate = async () => {
+ if (!selectedCertificate) return;
+
+ setIsSubmitting(true);
+ try {
+ await deleteSSLCertificate(selectedCertificate.id);
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowDeleteDialog(false);
+ setSelectedCertificate(null);
+ toast.success(t('certificateDeleted'));
+ } catch (error) {
+ console.error("Error deleting certificate:", error);
+ toast.error(t('failedToDeleteCertificate'));
+ } finally {
+ setIsSubmitting(false);
}
};
- const handleDeleteCertificate = (certificate: SSLCertificate) => {
- setCertToDelete(certificate);
- setDeleteConfirmOpen(true);
+ const openViewDialog = (certificate: SSLCertificate) => {
+ setSelectedCertificate(certificate);
+ setShowViewDialog(true);
};
- const confirmDelete = () => {
- if (certToDelete && onDelete) {
- onDelete(certToDelete);
- setDeleteConfirmOpen(false);
- setCertToDelete(null);
- }
+ const openEditDialog = (certificate: SSLCertificate) => {
+ setSelectedCertificate(certificate);
+ setShowEditDialog(true);
+ };
+
+ const openDeleteDialog = (certificate: SSLCertificate) => {
+ setSelectedCertificate(certificate);
+ setShowDeleteDialog(true);
};
return (
<>
-
- {refreshingId && (
-
-
-
- {t('checkingSSLCertificate')}
-
-
- )}
-
-
-
- {t('domain')}
- {t('issuer')}
- {t('expirationDate')}
- {t('daysLeft')}
- {t('status')}
- {t('lastNotified')}
- {t('actions')}
-
-
-
- {certificates.length === 0 ? (
+
+
+
+
-
- {t('noSSLCertificates')}
-
+ {t('domain')}
+ {t('status')}
+ {t('issuer')}
+ {t('validUntil')}
+ {t('daysLeft')}
+ Check Interval
+
- ) : (
- certificates.map((certificate) => (
+
+
+ {certificates.map((certificate) => (
- {certificate.domain}
- {certificate.issuer_o || t('unknown')}
+
+ {certificate.domain}
+
- {formatDate(certificate.valid_till)}
+
+ {certificate.issuer_o || certificate.issuer_cn || 'Unknown'}
- {typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
+ {certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
-
+
+ {certificate.days_left} {t('days')}
+
- {certificate.last_notified
- ? formatDate(certificate.last_notified)
- : t('never')}
+ {certificate.check_interval || 1} {t('days')}
-
-
-
-
-
-
- handleViewCertificate(certificate)}
- className="cursor-pointer"
- >
- {t('view')}
-
- {
- if (refreshingId === null) {
- onRefresh(certificate.id);
- }
- }}
- disabled={refreshingId !== null}
- className="cursor-pointer"
- >
-
- {t('check')}
-
- handleEditCertificate(certificate)}
- className="cursor-pointer"
- >
- {t('edit')}
-
- handleDeleteCertificate(certificate)}
- className="cursor-pointer text-destructive focus:text-destructive"
- >
- {t('delete')}
-
-
-
+
+
- ))
- )}
-
-
-
+ ))}
+
+
- {/* SSL Certificate Details Dialog */}
-