Files
checkcle/application/src/components/ssl-domain/SSLDomainContent.tsx
T
Tola Leng 614c10188d feat: Add check interval and check_at update
- Added a "Check Interval" field to the SSL certificate create/edit forms.
- Implemented functionality to update the `check_at` field in Pocketbase with the current time when the "Check" button is clicked.
2025-06-20 17:17:34 +07:00

286 lines
10 KiB
TypeScript

import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
import { SSLCertificatesTable } from "./SSLCertificatesTable";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
import { pb } from "@/lib/pocketbase";
import { useLanguage } from "@/contexts/LanguageContext";
export const SSLDomainContent = () => {
const { t } = useLanguage();
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
const queryClient = useQueryClient();
// Fetch SSL certificates with explicit error handling
const { data: certificates = [], isLoading, error } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: async () => {
try {
console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates();
console.log("Received SSL certificates:", result);
return result;
} catch (error) {
console.error("Error fetching certificates:", error);
toast.error(t('failedToLoadCertificates'));
throw error;
}
},
refetchOnWindowFocus: false,
refetchInterval: 300000, // Refresh every 5 minutes
});
// Add certificate mutation
const addMutation = useMutation({
mutationFn: addSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsAddDialogOpen(false);
toast.success(t('sslCertificateAdded'));
},
onError: (error) => {
console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
}
});
// Edit certificate mutation - Updated to ensure thresholds are properly updated
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
console.log("Updating certificate with data:", certificate);
// Create the update data object
const updateData = {
warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel,
};
console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
console.log("PocketBase update response:", updated);
// After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
return refreshedCert;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsEditDialogOpen(false);
setSelectedCertificate(null);
toast.success(t('sslCertificateUpdated'));
},
onError: (error) => {
console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
});
// Delete certificate mutation
const deleteMutation = useMutation({
mutationFn: deleteSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
toast.success(t('sslCertificateDeleted'));
},
onError: (error) => {
console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
});
// Refresh certificate mutation - Updated to ensure notifications are sent
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null);
toast.success(t('sslCertificateRefreshed').replace('{domain}', data.domain));
},
onError: (error) => {
console.error("Error refreshing SSL certificate:", error);
let errorMessage = t('failedToCheckCertificate');
if (error instanceof Error) {
errorMessage = error.message;
}
toast.error(errorMessage);
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
});
// Refresh all certificates mutation
const refreshAllMutation = useMutation({
mutationFn: refreshAllCertificates,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsRefreshingAll(false);
if (result.failed === 0) {
toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
} else {
toast.info(t('someCertificatesFailed')
.replace('{success}', result.success.toString())
.replace('{failed}', result.failed.toString()));
}
},
onError: (error) => {
console.error("Error refreshing all certificates:", error);
toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false);
// Still refresh the data to show any partial information
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
});
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
addMutation.mutate(data);
};
const handleRefreshCertificate = (id: string) => {
if (refreshingId) return; // Prevent multiple refreshes
setRefreshingId(id);
refreshMutation.mutate(id);
};
const handleEditCertificate = (certificate: SSLCertificate) => {
setSelectedCertificate(certificate);
setIsEditDialogOpen(true);
};
const handleUpdateCertificate = (certificate: SSLCertificate) => {
console.log("Handling certificate update with data:", certificate);
editMutation.mutate(certificate);
};
const handleDeleteCertificate = (certificate: SSLCertificate) => {
deleteMutation.mutate(certificate.id);
};
const handleRefreshAll = async () => {
if (certificates.length === 0) {
toast.info(t('noCertificatesToRefresh'));
return;
}
setIsRefreshingAll(true);
toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
refreshAllMutation.mutate();
};
if (isLoading) {
return <LoadingState />;
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>{t('failedToLoadCertificates')}</p>
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
{t('check')}
</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleRefreshAll}
disabled={isRefreshingAll || refreshingId !== null}
className="relative"
>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
{t('refreshAll')}
{isRefreshingAll && (
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
...
</span>
)}
</Button>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
</Button>
</div>
</div>
<SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable />
</div>
</div>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setIsAddDialogOpen(false)}
isPending={addMutation.isPending}
/>
</DialogContent>
</Dialog>
{selectedCertificate && (
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
</DialogHeader>
<EditSSLCertificateForm
certificate={selectedCertificate}
onSubmit={handleUpdateCertificate}
onCancel={() => {
setIsEditDialogOpen(false);
setSelectedCertificate(null);
}}
isPending={editMutation.isPending}
/>
</DialogContent>
</Dialog>
)}
</main>
);
};