Merge pull request #27 from operacle/develop

feat: Implement uptime monitoring data retention
This commit is contained in:
Tola Leng
2025-05-31 17:44:43 +07:00
committed by GitHub
parent aec7118496
commit dd758b9334
15 changed files with 902 additions and 265 deletions
@@ -117,10 +117,10 @@ export const Sidebar = ({
<BookOpen className="h-4 w-4 mr-2" />
<span className="text-sm">{t("alertsTemplates")}</span>
</Link>
<div className={getMenuItemClasses(false)}>
<Link to={`/settings?panel=data-retention`} className={getMenuItemClasses(activeSettingsItem === 'data-retention')} onClick={() => handleSettingsItemClick('data-retention')}>
<Database className="h-4 w-4 mr-2" />
<span className="text-sm">{t("dataRetention")}</span>
</div>
</Link>
<Link to={`/settings?panel=about`} className={getMenuItemClasses(activeSettingsItem === 'about')} onClick={() => handleSettingsItemClick('about')}>
<Info className="h-4 w-4 mr-2" />
<span className="text-sm">{t("aboutSystem")}</span>
@@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast";
import { authService } from "@/services/authService";
import { AlertCircle, CheckCircle } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useNavigate } from "react-router-dom";
// Profile update form schema
const profileFormSchema = z.object({
@@ -33,10 +34,10 @@ interface UpdateProfileFormProps {
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [emailChangeRequested, setEmailChangeRequested] = useState(false);
const [updateError, setUpdateError] = useState<string | null>(null);
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
const { toast } = useToast();
const navigate = useNavigate();
// Initialize the form with current user data
const form = useForm<ProfileFormValues>({
@@ -52,7 +53,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
setIsSubmitting(true);
setUpdateError(null);
setUpdateSuccess(null);
setEmailChangeRequested(false);
try {
console.log("Submitting profile update with data:", data);
@@ -75,19 +75,25 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
// Update user data using the userService
await userService.updateUser(user.id, updateData);
// Refresh user data in auth context
await authService.refreshUserData();
// If email was changed, show a specific message
// If email was changed, show success message and auto-logout
if (isEmailChanged) {
setEmailChangeRequested(true);
setUpdateSuccess("A verification email has been sent to your new email address. Please check your inbox to complete the change.");
setUpdateSuccess("Email changed successfully! You will be logged out for security reasons. Please log in again with your new email.");
toast({
title: "Email verification sent",
description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.",
title: "Email changed successfully",
description: "You will be logged out for security reasons. Please log in again with your new email.",
variant: "default",
});
// Auto-logout after 3 seconds
setTimeout(() => {
authService.logout();
navigate("/login");
}, 3000);
} else {
// Refresh user data in auth context for other field changes
await authService.refreshUserData();
setUpdateSuccess("Your profile information has been updated successfully.");
toast({
title: "Profile updated",
@@ -132,16 +138,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
</AlertDescription>
</Alert>
)}
{emailChangeRequested && (
<Alert className="bg-yellow-50 border-yellow-200 text-yellow-800">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<AlertDescription>
A verification email has been sent to your new email address.
Please check your inbox and follow the instructions to complete the change.
</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
@@ -183,7 +179,7 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
<FormMessage />
{field.value !== user.email && (
<p className="text-xs text-muted-foreground mt-1">
Changing your email requires verification. A verification email will be sent.
Changing your email will log you out for security reasons. You will need to log in again with your new email.
</p>
)}
</FormItem>
@@ -14,18 +14,17 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { format, subDays, subHours, subMonths, subWeeks, subYears } from "date-fns";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type DateRangeOption = '60min' | '24h' | '7d' | '30d' | '1y' | 'custom';
export type DateRangeOption = '24h' | '7d' | '30d' | '1y' | 'custom';
interface DateRangeFilterProps {
onRangeChange: (startDate: Date, endDate: Date, option: DateRangeOption) => void;
selectedOption?: DateRangeOption;
}
// Define a proper type for the date range
interface DateRange {
from: Date | undefined;
to: Date | undefined;
@@ -45,57 +44,48 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
const now = new Date();
let startDate: Date;
let endDate: Date = new Date(now.getTime() + (5 * 60 * 1000)); // Add 5 minutes buffer to future
switch (option) {
case '60min':
// Ensure we're getting exactly 60 minutes ago
startDate = new Date(now.getTime() - 60 * 60 * 1000);
console.log(`60min option selected: ${startDate.toISOString()} to ${now.toISOString()}`);
break;
case '24h':
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000));
break;
case '7d':
startDate = subDays(now, 7);
startDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
break;
case '30d':
startDate = subDays(now, 30);
startDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
break;
case '1y':
startDate = subYears(now, 1);
startDate = new Date(now.getTime() - (365 * 24 * 60 * 60 * 1000));
break;
case 'custom':
// Don't trigger onRangeChange for custom until both dates are selected
setIsCalendarOpen(true);
return;
default:
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000)); // Default to 24 hours
}
console.log(`DateRangeFilter: Option changed to ${option}, date range: ${startDate.toISOString()} to ${now.toISOString()}`);
onRangeChange(startDate, now, option);
console.log(`DateRangeFilter: ${option} selected, range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
onRangeChange(startDate, endDate, option);
};
// Handle custom date range selection
const handleCustomRangeSelect = (range: DateRange | undefined) => {
if (!range) {
if (!range || !range.from || !range.to) {
return;
}
setCustomDateRange(range);
if (range.from && range.to) {
// Ensure that we have both from and to dates before triggering the change
const startOfDay = new Date(range.from);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(range.to);
endOfDay.setHours(23, 59, 59, 999);
console.log(`DateRangeFilter: Custom range selected: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
onRangeChange(startOfDay, endOfDay, 'custom');
setIsCalendarOpen(false);
}
const startOfDay = new Date(range.from);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(range.to);
endOfDay.setHours(23, 59, 59, 999);
console.log(`Custom range: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
onRangeChange(startOfDay, endOfDay, 'custom');
setIsCalendarOpen(false);
};
return (
@@ -105,7 +95,6 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
<SelectValue placeholder="Select time range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="60min">Last 60 minutes</SelectItem>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>
@@ -153,4 +142,4 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
)}
</div>
);
}
}
@@ -5,6 +5,7 @@ import { Service, UptimeData } from "@/types/service.types";
import { useToast } from "@/hooks/use-toast";
import { useNavigate } from "react-router-dom";
import { uptimeService } from "@/services/uptimeService";
import { DateRangeOption } from "../../DateRangeFilter";
export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
const [service, setService] = useState<Service | null>(null);
@@ -13,15 +14,12 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const { toast } = useToast();
const navigate = useNavigate();
// Handler for service status changes
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
if (!service || !serviceId) return;
try {
// Optimistic UI update
setService({ ...service, status: newStatus as Service["status"] });
// Update the service status in PocketBase
await pb.collection('services').update(serviceId, {
status: newStatus
});
@@ -32,7 +30,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
});
} catch (error) {
console.error("Failed to update service status:", error);
// Revert the optimistic update
setService(prevService => prevService);
toast({
@@ -43,51 +40,30 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
}
};
// Function to fetch uptime data with date filters
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange: string) => {
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string) => {
try {
console.log(`Fetching uptime data from ${start.toISOString()} to ${end.toISOString()}`);
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}`);
// Set appropriate limits based on time range to ensure enough granularity
let limit = 200; // default
let limit = 500; // Default limit
// Adjust limits based on selected range
if (selectedRange === '60min') {
limit = 300; // More points for shorter time ranges
} else if (selectedRange === '24h') {
limit = 200;
if (selectedRange === '24h') {
limit = 300;
} else if (selectedRange === '7d') {
limit = 250;
} else if (selectedRange === '30d' || selectedRange === '1y') {
limit = 300; // More points for longer time ranges
limit = 400;
}
console.log(`Using limit ${limit} for range ${selectedRange}`);
const history = await uptimeService.getUptimeHistory(serviceId, limit, start, end);
console.log(`Fetched ${history.length} uptime records for time range ${selectedRange}`);
console.log(`Retrieved ${history.length} uptime records`);
if (history.length === 0) {
console.log("No data returned from API, checking if we need to fetch with a higher limit");
// If no data, try with a higher limit as fallback
if (limit < 500) {
const extendedHistory = await uptimeService.getUptimeHistory(serviceId, 500, start, end);
console.log(`Fallback: Fetched ${extendedHistory.length} uptime records with higher limit`);
if (extendedHistory.length > 0) {
setUptimeData(extendedHistory);
return extendedHistory;
}
}
}
// Sort data by timestamp (newest first)
const sortedHistory = [...history].sort((a, b) =>
// Sort by timestamp (newest first)
const filteredHistory = [...history].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
setUptimeData(sortedHistory);
return sortedHistory;
setUptimeData(filteredHistory);
return filteredHistory;
} catch (error) {
console.error("Error fetching uptime data:", error);
toast({
@@ -110,7 +86,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
setIsLoading(true);
// Add a timeout to prevent hanging
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), 10000);
});
@@ -136,7 +111,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
setService(formattedService);
// Fetch uptime history with date range
// Fetch initial uptime history with 24h default
await fetchUptimeData(serviceId, startDate, endDate, '24h');
} catch (error) {
console.error("Error fetching service:", error);
@@ -156,10 +131,11 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
// Update data when date range changes
useEffect(() => {
if (serviceId && !isLoading) {
fetchUptimeData(serviceId, startDate, endDate, '24h');
if (serviceId && !isLoading && service) {
console.log(`Date range changed, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate);
}
}, [startDate, endDate]);
}, [startDate, endDate, serviceId, isLoading, service]);
return {
service,
@@ -170,4 +146,4 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
handleStatusChange,
fetchUptimeData
};
};
};
@@ -1,4 +1,3 @@
import { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { DateRangeOption } from "../DateRangeFilter";
@@ -12,13 +11,17 @@ export const ServiceDetailContainer = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Ensure we use exact timestamp for startDate
// Set default to 24h
const [startDate, setStartDate] = useState<Date>(() => {
const date = new Date();
date.setHours(date.getHours() - 24);
date.setHours(date.getHours() - 24); // Go back 24 hours
return date;
});
const [endDate, setEndDate] = useState<Date>(() => {
const date = new Date();
date.setMinutes(date.getMinutes() + 5); // Add 5 minutes buffer to future
return date;
});
const [endDate, setEndDate] = useState<Date>(new Date());
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
// State for sidebar collapse functionality (shared with Dashboard)
@@ -91,14 +94,16 @@ export const ServiceDetailContainer = () => {
// Handle date range filter changes
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
console.log(`Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
console.log(`ServiceDetailContainer: Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
// Update state which will trigger the useEffect in useServiceData
setStartDate(start);
setEndDate(end);
setSelectedRange(option);
// Refetch uptime data with new date range, passing the selected range option
// Also explicitly fetch data with the new range to ensure immediate update
if (id) {
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
fetchUptimeData(id, start, end, option);
}
}, [id, fetchUptimeData]);
@@ -123,4 +128,4 @@ export const ServiceDetailContainer = () => {
)}
</ServiceDetailWrapper>
);
};
};
@@ -56,7 +56,12 @@ export const ServiceRow = ({
<ServiceRowResponseTime responseTime={service.responseTime} />
</TableCell>
<TableCell className="w-52 py-4">
<UptimeBar uptime={service.uptime} status={service.status} serviceId={service.id} />
<UptimeBar
uptime={service.uptime}
status={service.status}
serviceId={service.id}
interval={service.interval}
/>
</TableCell>
<TableCell className="py-4">
<LastCheckedTime
@@ -1,4 +1,3 @@
import React, { useState, useEffect } from "react";
import { Progress } from "@/components/ui/progress";
import { Check, X, AlertTriangle, Pause, Clock, Info, RefreshCcw } from "lucide-react";
@@ -17,17 +16,18 @@ import { Button } from "@/components/ui/button";
interface UptimeBarProps {
uptime: number;
status: string;
serviceId?: string; // Optional serviceId to fetch history
serviceId?: string;
interval?: number; // Service monitoring interval in seconds
}
export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
export const UptimeBar = ({ uptime, status, serviceId, interval = 60 }: UptimeBarProps) => {
const { theme } = useTheme();
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
// Fetch real uptime history data if serviceId is provided with improved caching and error handling
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['uptimeHistory', serviceId],
queryFn: () => serviceId ? uptimeService.getUptimeHistory(serviceId, 20) : Promise.resolve([]),
queryFn: () => serviceId ? uptimeService.getUptimeHistory(serviceId, 50) : Promise.resolve([]),
enabled: !!serviceId,
refetchInterval: 30000, // Refresh every 30 seconds
staleTime: 15000, // Consider data fresh for 15 seconds
@@ -36,10 +36,45 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential backoff with max 10s
});
// Filter uptime data to respect the service interval
const filterUptimeDataByInterval = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
if (!data || data.length === 0) return [];
// Sort data by timestamp (newest first)
const sortedData = [...data].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
const filtered: UptimeData[] = [];
let lastIncludedTime: number | null = null;
const intervalMs = intervalSeconds * 1000; // Convert to milliseconds
// Include the most recent record first
if (sortedData.length > 0) {
filtered.push(sortedData[0]);
lastIncludedTime = new Date(sortedData[0].timestamp).getTime();
}
// Filter subsequent records to maintain proper interval spacing
for (let i = 1; i < sortedData.length && filtered.length < 20; i++) {
const currentTime = new Date(sortedData[i].timestamp).getTime();
// Only include if enough time has passed since the last included record
if (lastIncludedTime && (lastIncludedTime - currentTime) >= intervalMs) {
filtered.push(sortedData[i]);
lastIncludedTime = currentTime;
}
}
return filtered;
};
// Update history items when data changes
useEffect(() => {
if (uptimeData && uptimeData.length > 0) {
setHistoryItems(uptimeData);
// Filter data based on the service interval
const filteredData = filterUptimeDataByInterval(uptimeData, interval);
setHistoryItems(filteredData);
} else if (status === "paused" || (uptimeData && uptimeData.length === 0)) {
// For paused services with no history, or empty history data, show all as paused
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
@@ -49,13 +84,13 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
id: `placeholder-${index}`,
serviceId: serviceId || "",
timestamp: new Date().toISOString(),
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
status: statusValue as "up" | "down" | "warning" | "paused",
responseTime: 0
}));
setHistoryItems(placeholderHistory);
}
}, [uptimeData, serviceId, status]);
}, [uptimeData, serviceId, status, interval]);
// Get appropriate color classes for each status type
const getStatusColor = (itemStatus: string) => {
@@ -153,13 +188,19 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
status as "up" | "down" | "warning" | "paused" : "paused";
const paddingItems: UptimeData[] = Array(20 - displayItems.length).fill(null).map((_, index) => ({
id: `padding-${index}`,
serviceId: serviceId || "",
timestamp: new Date().toISOString(),
status: lastStatus,
responseTime: 0
}));
// Generate padding items with proper time spacing
const paddingItems: UptimeData[] = Array(20 - displayItems.length).fill(null).map((_, index) => {
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
const timeOffset = (index + 1) * interval * 1000; // Respect the interval
return {
id: `padding-${index}`,
serviceId: serviceId || "",
timestamp: new Date(baseTime - timeOffset).toISOString(),
status: lastStatus,
responseTime: 0
};
});
displayItems.push(...paddingItems);
}
@@ -201,10 +242,10 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
{Math.round(uptime)}% uptime
</span>
<span className="text-xs text-muted-foreground">
Last 20 checks
Last 20 checks
</span>
</div>
</div>
</TooltipProvider>
);
}
}
@@ -1,30 +1,51 @@
import React from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import React, { useEffect, useState } from 'react';
import { format } from 'date-fns';
import {
Card, CardContent, CardDescription, CardHeader, CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon } from "lucide-react";
import {
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useLanguage } from "@/contexts/LanguageContext";
import { useTheme } from "@/contexts/ThemeContext";
import { useSystemSettings } from "@/hooks/useSystemSettings";
export const AboutSystem: React.FC = () => {
const {
t
} = useLanguage();
const {
theme
} = useTheme();
const {
systemName
} = useSystemSettings();
return <div className="space-y-6 animate-fade-in">
const { t } = useLanguage();
const { theme } = useTheme();
const { systemName } = useSystemSettings();
const [version, setVersion] = useState<string>('...');
const [releaseDate, setReleaseDate] = useState<string>('...');
useEffect(() => {
const fetchLatestRelease = async () => {
try {
const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
const data = await res.json();
setVersion(data.tag_name || 'v1.x.x');
setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
} catch (err) {
setVersion('v1.x.x');
setReleaseDate(t('unknown'));
}
};
fetchLatestRelease();
}, [t]);
return (
<div className="space-y-6 animate-fade-in">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
<p className="text-muted-foreground text-base leading-relaxed mt-2">{t('aboutCheckcle')}</p>
<p className="text-muted-foreground text-base leading-relaxed mt-2">
{t('aboutCheckcle')}
</p>
</div>
<Separator />
<div className="grid gap-8 md:grid-cols-2">
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
@@ -38,7 +59,7 @@ export const AboutSystem: React.FC = () => {
<div className="flex flex-col space-y-3 pt-2">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('systemVersion')}</span>
<span className="text-foreground font-medium">{t('version')} 1.1.0</span>
<span className="text-foreground font-medium">{version}</span>
</div>
<Separator className="my-1" />
<div className="flex justify-between items-center">
@@ -48,20 +69,22 @@ export const AboutSystem: React.FC = () => {
<Separator className="my-1" />
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('releasedOn')}</span>
<span className="text-foreground font-medium">May 16, 2025</span>
<span className="text-foreground font-medium">{releaseDate}</span>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
<CardTitle className="flex items-center gap-2">
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
<span>{t('links')}</span>
</CardTitle>
<CardDescription className="font-medium text-base">{systemName || 'ReamStack'} {t('resources').toLowerCase()}</CardDescription>
<CardDescription className="font-medium text-base">
{systemName || 'ReamStack'} {t('resources').toLowerCase()}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="grid grid-cols-1 gap-3">
@@ -85,6 +108,8 @@ export const AboutSystem: React.FC = () => {
</CardContent>
</Card>
</div>
</div>;
</div>
);
};
export default AboutSystem;
export default AboutSystem;
@@ -0,0 +1,316 @@
import React, { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2, Database, Trash2, AlertTriangle, Globe, Server } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
import { authService } from "@/services/authService";
import { dataRetentionService } from "@/services/dataRetentionService";
interface RetentionSettings {
uptimeRetentionDays: number;
serverRetentionDays: number;
}
const DataRetentionSettings = () => {
const { t } = useLanguage();
const { toast } = useToast();
const [settings, setSettings] = useState<RetentionSettings>({
uptimeRetentionDays: 30,
serverRetentionDays: 30
});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isUptimeShrinking, setIsUptimeShrinking] = useState(false);
const [isServerShrinking, setIsServerShrinking] = useState(false);
const [isFullShrinking, setIsFullShrinking] = useState(false);
const [lastCleanup, setLastCleanup] = useState<string | null>(null);
// Check if user is super admin
const currentUser = authService.getCurrentUser();
const isSuperAdmin = currentUser?.role === "superadmin";
useEffect(() => {
if (isSuperAdmin) {
loadSettings();
}
}, [isSuperAdmin]);
const loadSettings = async () => {
try {
setIsLoading(true);
const result = await dataRetentionService.getRetentionSettings();
if (result) {
setSettings({
uptimeRetentionDays: result.uptimeRetentionDays || 30,
serverRetentionDays: result.serverRetentionDays || 30
});
setLastCleanup(result.lastCleanup);
}
} catch (error) {
console.error("Error loading retention settings:", error);
toast({
title: "Error",
description: "Failed to load retention settings",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
try {
setIsSaving(true);
await dataRetentionService.updateRetentionSettings(settings);
toast({
title: "Settings saved",
description: "Data retention settings have been updated",
});
} catch (error) {
console.error("Error saving retention settings:", error);
toast({
title: "Error",
description: "Failed to save retention settings",
variant: "destructive",
});
} finally {
setIsSaving(false);
}
};
const handleUptimeShrink = async () => {
try {
setIsUptimeShrinking(true);
const result = await dataRetentionService.manualUptimeCleanup();
toast({
title: "Uptime cleanup completed",
description: `Deleted ${result.deletedRecords} old uptime records`,
});
// Reload settings to get updated last cleanup time
await loadSettings();
} catch (error) {
console.error("Error during uptime cleanup:", error);
toast({
title: "Error",
description: "Failed to perform uptime data cleanup",
variant: "destructive",
});
} finally {
setIsUptimeShrinking(false);
}
};
const handleServerShrink = async () => {
try {
setIsServerShrinking(true);
const result = await dataRetentionService.manualServerCleanup();
toast({
title: "Server cleanup completed",
description: `Deleted ${result.deletedRecords} old server records`,
});
// Reload settings to get updated last cleanup time
await loadSettings();
} catch (error) {
console.error("Error during server cleanup:", error);
toast({
title: "Error",
description: "Failed to perform server data cleanup",
variant: "destructive",
});
} finally {
setIsServerShrinking(false);
}
};
const handleFullShrink = async () => {
try {
setIsFullShrinking(true);
const result = await dataRetentionService.manualCleanup();
toast({
title: "Database cleanup completed",
description: `Deleted ${result.deletedRecords} old records`,
});
// Reload settings to get updated last cleanup time
await loadSettings();
} catch (error) {
console.error("Error during manual cleanup:", error);
toast({
title: "Error",
description: "Failed to perform database cleanup",
variant: "destructive",
});
} finally {
setIsFullShrinking(false);
}
};
// Show permission notice for admin users
if (!isSuperAdmin) {
return (
<div className="p-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
{t("dataRetention", "settings")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
</AlertDescription>
</Alert>
</CardContent>
</Card>
</div>
);
}
if (isLoading) {
return (
<div className="p-4 flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
Loading retention settings...
</div>
);
}
return (
<div className="p-4 space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Data Retention Settings
</CardTitle>
<CardDescription>
Configure how long monitoring data is kept in the system
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="uptimeRetention">Uptime Monitoring Retention (days)</Label>
<Input
id="uptimeRetention"
type="number"
min="1"
max="365"
value={settings.uptimeRetentionDays}
onChange={(e) => setSettings(prev => ({
...prev,
uptimeRetentionDays: parseInt(e.target.value) || 30
}))}
className="mt-1"
/>
<p className="text-sm text-muted-foreground mt-1">
Service uptime and incident data older than this will be automatically deleted
</p>
</div>
<div>
<Label htmlFor="serverRetention">Server Monitoring Retention (days)</Label>
<Input
id="serverRetention"
type="number"
min="1"
max="365"
value={settings.serverRetentionDays}
onChange={(e) => setSettings(prev => ({
...prev,
serverRetentionDays: parseInt(e.target.value) || 30
}))}
className="mt-1"
/>
<p className="text-sm text-muted-foreground mt-1">
Server metrics and process data older than this will be automatically deleted
</p>
</div>
</div>
{lastCleanup && (
<Alert>
<Database className="h-4 w-4" />
<AlertDescription>
Last automatic cleanup: {new Date(lastCleanup).toLocaleString()}
</AlertDescription>
</Alert>
)}
</CardContent>
<CardFooter className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2 w-full">
<Button
variant="outline"
onClick={handleUptimeShrink}
disabled={isUptimeShrinking}
className="flex items-center gap-2"
>
{isUptimeShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Globe className="h-4 w-4" />
)}
Shrink Uptime Data
</Button>
<Button
variant="outline"
onClick={handleServerShrink}
disabled={isServerShrinking}
className="flex items-center gap-2"
>
{isServerShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Server className="h-4 w-4" />
)}
Shrink Server Data
</Button>
<Button
variant="outline"
onClick={handleFullShrink}
disabled={isFullShrinking}
className="flex items-center gap-2"
>
{isFullShrinking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
Full Database Shrink
</Button>
</div>
<div className="flex justify-end w-full">
<Button
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2"
>
{isSaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Save Changes
</Button>
</div>
</CardFooter>
</Card>
</div>
);
};
export default DataRetentionSettings;
@@ -0,0 +1,5 @@
import DataRetentionSettings from './DataRetentionSettings';
export default DataRetentionSettings;
export { DataRetentionSettings };
@@ -4,6 +4,8 @@ import { useToast } from "@/hooks/use-toast";
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
import { UserFormValues, NewUserFormValues } from "../userForms";
import { avatarOptions } from "../avatarOptions";
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
export const useUserOperations = (
fetchUsers: () => Promise<void>,
@@ -15,6 +17,7 @@ export const useUserOperations = (
newUserFormReset: (values: any) => void
) => {
const { toast } = useToast();
const navigate = useNavigate();
const handleDeleteUser = async (userToDelete: User | null) => {
if (!userToDelete) return;
@@ -47,6 +50,11 @@ export const useUserOperations = (
setUpdateError(null);
try {
// Get current logged-in user to check if we're editing ourselves
const loggedInUser = authService.getCurrentUser();
const isEditingSelf = loggedInUser?.id === currentUser.id;
const isEmailChanged = data.email !== currentUser.email;
// Create update object with only the fields we want to update
const updateData: UpdateUserData = {
full_name: data.full_name,
@@ -57,7 +65,6 @@ export const useUserOperations = (
};
// For avatar, only include if it's different from current one
// Note: We're still sending the avatar path, but our updated userService will handle it properly
if (data.avatar && data.avatar !== currentUser.avatar) {
updateData.avatar = data.avatar;
}
@@ -66,9 +73,26 @@ export const useUserOperations = (
await userService.updateUser(currentUser.id, updateData);
// After successful update, refresh the auth user data if this is the current user
// In a real app, you'd check if the updated user is the current logged-in user
// Handle email change for current user
if (isEditingSelf && isEmailChanged) {
toast({
title: "Email changed successfully",
description: "You will be logged out for security reasons. Please log in again with your new email.",
variant: "default",
});
setIsDialogOpen(false);
// Auto-logout after 2 seconds if editing own email
setTimeout(() => {
authService.logout();
navigate("/login");
}, 2000);
return; // Don't continue with normal flow
}
// Normal success flow for other users or non-email changes
toast({
title: "User updated",
description: `${data.full_name || data.username}'s profile has been updated.`,
@@ -155,4 +179,4 @@ export const useUserOperations = (
onSubmit,
onAddUser,
};
};
};