Refactor: Separate data display by monitoring source
and Fix: Display data collection in detail page
This commit is contained in:
@@ -250,7 +250,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
|||||||
<Textarea
|
<Textarea
|
||||||
readOnly
|
readOnly
|
||||||
value={`# One-click installation command:
|
value={`# One-click installation command:
|
||||||
curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
|
curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- \\
|
||||||
--region-name="${regionName}" \\
|
--region-name="${regionName}" \\
|
||||||
--agent-id="${installCommand.agent_id}" \\
|
--agent-id="${installCommand.agent_id}" \\
|
||||||
--agent-ip="${agentIp}" \\
|
--agent-ip="${agentIp}" \\
|
||||||
@@ -262,7 +262,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
|
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
|
||||||
>
|
>
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -395,7 +395,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
|
|||||||
<div className="border-l-4 border-blue-500 pl-4">
|
<div className="border-l-4 border-blue-500 pl-4">
|
||||||
<p className="font-medium">Step 1: Download Package</p>
|
<p className="font-medium">Step 1: Download Package</p>
|
||||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
|
wget https://github.com/checkcle/distributed-regional-monitoring/releases/latest/download/distributed-regional-check-agent_1.0.0_amd64.deb
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { regionalService } from "@/services/regionalService";
|
||||||
|
import { MapPin, Loader2, Globe } from "lucide-react";
|
||||||
|
|
||||||
|
interface RegionalAgentFilterProps {
|
||||||
|
selectedAgent: string;
|
||||||
|
onAgentChange: (agent: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAgentFilterProps) {
|
||||||
|
const { data: regionalAgents = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['regional-services'],
|
||||||
|
queryFn: regionalService.getRegionalServices,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter only online agents
|
||||||
|
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
|
||||||
|
|
||||||
|
const getCurrentAgentDisplay = () => {
|
||||||
|
if (!selectedAgent || selectedAgent === "default") {
|
||||||
|
return "Default Monitoring";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [regionName] = selectedAgent.split("|");
|
||||||
|
const agent = onlineAgents.find(agent =>
|
||||||
|
`${agent.region_name}|${agent.agent_id}` === selectedAgent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
return `${agent.region_name} (${agent.agent_ip_address})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return regionName || "Default Monitoring";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64">
|
||||||
|
<label className="text-sm font-medium flex items-center gap-2 mb-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
Monitoring Source
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
onValueChange={onAgentChange}
|
||||||
|
value={selectedAgent || "default"}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={
|
||||||
|
isLoading
|
||||||
|
? "Loading agents..."
|
||||||
|
: getCurrentAgentDisplay()
|
||||||
|
} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<SelectItem value="loading" disabled>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Loading agents...
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectItem value="default">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="font-medium">Default Monitoring</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
{onlineAgents.length > 0 && onlineAgents.map((agent) => (
|
||||||
|
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="font-medium">{agent.region_name}</span>
|
||||||
|
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{onlineAgents.length === 0 && !isLoading && (
|
||||||
|
<p className="text-xs text-amber-600 mt-1">
|
||||||
|
No regional agents available. Using default monitoring only.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+43
-10
@@ -11,6 +11,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
const [service, setService] = useState<Service | null>(null);
|
const [service, setService] = useState<Service | null>(null);
|
||||||
const [uptimeData, setUptimeData] = useState<UptimeData[]>([]);
|
const [uptimeData, setUptimeData] = useState<UptimeData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [selectedRegionalAgent, setSelectedRegionalAgent] = useState<string>("default");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -40,14 +41,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string) => {
|
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
|
||||||
try {
|
try {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
console.log('No service data available for uptime fetch');
|
console.log('No service data available for uptime fetch');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}, service type: ${service.type}`);
|
const currentAgent = regionalAgent || selectedRegionalAgent;
|
||||||
|
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}, service type: ${service.type}, regional agent: ${currentAgent}`);
|
||||||
|
|
||||||
|
// Clear existing data immediately when switching agents
|
||||||
|
setUptimeData([]);
|
||||||
|
|
||||||
let limit = 500; // Default limit
|
let limit = 500; // Default limit
|
||||||
|
|
||||||
@@ -59,15 +64,27 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
|
|
||||||
console.log(`Using limit ${limit} for range ${selectedRange}`);
|
console.log(`Using limit ${limit} for range ${selectedRange}`);
|
||||||
|
|
||||||
// Use the service type to fetch from the correct collection
|
let history: UptimeData[] = [];
|
||||||
const history = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
|
|
||||||
console.log(`Retrieved ${history.length} uptime records from collection for ${service.type} service`);
|
if (currentAgent === "default") {
|
||||||
|
// Fetch default monitoring data - this will automatically filter out regional records
|
||||||
|
console.log(`Fetching default monitoring data from ${service.type} collection`);
|
||||||
|
history = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
|
||||||
|
console.log(`Retrieved ${history.length} default monitoring records`);
|
||||||
|
} else {
|
||||||
|
// Fetch regional agent specific data
|
||||||
|
const [regionName, agentId] = currentAgent.split("|");
|
||||||
|
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
|
||||||
|
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
|
||||||
|
console.log(`Retrieved ${history.length} regional monitoring records`);
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by timestamp (newest first)
|
// Sort by timestamp (newest first)
|
||||||
const filteredHistory = [...history].sort((a, b) =>
|
const filteredHistory = [...history].sort((a, b) =>
|
||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "default" ? "default" : "regional"} monitoring`);
|
||||||
setUptimeData(filteredHistory);
|
setUptimeData(filteredHistory);
|
||||||
return filteredHistory;
|
return filteredHistory;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -81,6 +98,20 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegionalAgentChange = (agent: string) => {
|
||||||
|
console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
|
||||||
|
|
||||||
|
// Clear data immediately when switching
|
||||||
|
setUptimeData([]);
|
||||||
|
setSelectedRegionalAgent(agent);
|
||||||
|
|
||||||
|
// Refetch data with new agent selection
|
||||||
|
if (serviceId && !isLoading && service) {
|
||||||
|
console.log(`Refetching data for new agent: ${agent}`);
|
||||||
|
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initial data loading
|
// Initial data loading
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchServiceData = async () => {
|
const fetchServiceData = async () => {
|
||||||
@@ -121,8 +152,8 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
|
console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
|
||||||
setService(formattedService);
|
setService(formattedService);
|
||||||
|
|
||||||
// Fetch initial uptime history with 24h default - wait for service to be set
|
// Small delay to ensure state is updated before fetching uptime data
|
||||||
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay to ensure state is updated
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching service:", error);
|
console.error("Error fetching service:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -143,9 +174,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serviceId && !isLoading && service) {
|
if (serviceId && !isLoading && service) {
|
||||||
console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||||
fetchUptimeData(serviceId, startDate, endDate, '24h');
|
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
|
||||||
}
|
}
|
||||||
}, [startDate, endDate, serviceId, isLoading, service]);
|
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
@@ -154,6 +185,8 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
setUptimeData,
|
setUptimeData,
|
||||||
isLoading,
|
isLoading,
|
||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
fetchUptimeData
|
fetchUptimeData,
|
||||||
|
selectedRegionalAgent,
|
||||||
|
handleRegionalAgentChange
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -80,7 +80,9 @@ export const ServiceDetailContainer = () => {
|
|||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
fetchUptimeData,
|
fetchUptimeData,
|
||||||
setService,
|
setService,
|
||||||
setUptimeData
|
setUptimeData,
|
||||||
|
selectedRegionalAgent,
|
||||||
|
handleRegionalAgentChange
|
||||||
} = useServiceData(id, startDate, endDate);
|
} = useServiceData(id, startDate, endDate);
|
||||||
|
|
||||||
// Set up real-time updates
|
// Set up real-time updates
|
||||||
@@ -104,9 +106,9 @@ export const ServiceDetailContainer = () => {
|
|||||||
// Also explicitly fetch data with the new range to ensure immediate update
|
// Also explicitly fetch data with the new range to ensure immediate update
|
||||||
if (id) {
|
if (id) {
|
||||||
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
|
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
|
||||||
fetchUptimeData(id, start, end, option);
|
fetchUptimeData(id, start, end, option, selectedRegionalAgent);
|
||||||
}
|
}
|
||||||
}, [id, fetchUptimeData]);
|
}, [id, fetchUptimeData, selectedRegionalAgent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceDetailWrapper
|
<ServiceDetailWrapper
|
||||||
@@ -124,6 +126,8 @@ export const ServiceDetailContainer = () => {
|
|||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
onStatusChange={handleStatusChange}
|
onStatusChange={handleStatusChange}
|
||||||
selectedDateOption={selectedRange}
|
selectedDateOption={selectedRange}
|
||||||
|
selectedRegionalAgent={selectedRegionalAgent}
|
||||||
|
onRegionalAgentChange={handleRegionalAgentChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ServiceDetailWrapper>
|
</ServiceDetailWrapper>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ interface ServiceDetailContentProps {
|
|||||||
onDateRangeChange: (start: Date, end: Date, option: DateRangeOption) => void;
|
onDateRangeChange: (start: Date, end: Date, option: DateRangeOption) => void;
|
||||||
onStatusChange: (newStatus: "up" | "down" | "paused" | "warning") => void;
|
onStatusChange: (newStatus: "up" | "down" | "paused" | "warning") => void;
|
||||||
selectedDateOption: DateRangeOption;
|
selectedDateOption: DateRangeOption;
|
||||||
|
selectedRegionalAgent: string;
|
||||||
|
onRegionalAgentChange: (agent: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceDetailContent = ({
|
export const ServiceDetailContent = ({
|
||||||
@@ -21,28 +23,37 @@ export const ServiceDetailContent = ({
|
|||||||
uptimeData,
|
uptimeData,
|
||||||
onDateRangeChange,
|
onDateRangeChange,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
selectedDateOption
|
selectedDateOption,
|
||||||
|
selectedRegionalAgent,
|
||||||
|
onRegionalAgentChange
|
||||||
}: ServiceDetailContentProps) => {
|
}: ServiceDetailContentProps) => {
|
||||||
// Check if data is available
|
// Check if data is available
|
||||||
const hasUptimeData = uptimeData && uptimeData.length > 0;
|
const hasUptimeData = uptimeData && uptimeData.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-6 pb-0 h-full overflow-auto">
|
<div className="p-4 md:p-6 pb-0 h-full overflow-auto">
|
||||||
<ServiceHeader service={service} onStatusChange={onStatusChange} />
|
<ServiceHeader
|
||||||
|
service={service}
|
||||||
|
onStatusChange={onStatusChange}
|
||||||
|
selectedRegionalAgent={selectedRegionalAgent}
|
||||||
|
onRegionalAgentChange={onRegionalAgentChange}
|
||||||
|
/>
|
||||||
<ServiceStatsCards service={service} uptimeData={uptimeData} />
|
<ServiceStatsCards service={service} uptimeData={uptimeData} />
|
||||||
|
|
||||||
<div className="mb-4 md:mb-6 mt-6 md:mt-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-4 md:gap-0">
|
<div className="mb-4 md:mb-6 mt-6 md:mt-8 flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
||||||
<h2 className="text-lg md:text-xl font-medium">Response Time History</h2>
|
<h2 className="text-lg md:text-xl font-medium">Response Time History</h2>
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-2">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-2">
|
||||||
Collection: {service.type.toLowerCase() === 'ping' || service.type.toLowerCase() === 'icmp' ? 'ping_data' :
|
<span className="text-sm text-muted-foreground">
|
||||||
service.type.toLowerCase() === 'dns' ? 'dns_data' :
|
Collection: {service.type.toLowerCase() === 'ping' || service.type.toLowerCase() === 'icmp' ? 'ping_data' :
|
||||||
service.type.toLowerCase() === 'tcp' ? 'tcp_data' : 'uptime_data'}
|
service.type.toLowerCase() === 'dns' ? 'dns_data' :
|
||||||
</span>
|
service.type.toLowerCase() === 'tcp' ? 'tcp_data' : 'uptime_data'}
|
||||||
<DateRangeFilter
|
</span>
|
||||||
onRangeChange={onDateRangeChange}
|
<DateRangeFilter
|
||||||
selectedOption={selectedDateOption}
|
onRangeChange={onDateRangeChange}
|
||||||
/>
|
selectedOption={selectedDateOption}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,7 +64,8 @@ export const ServiceDetailContent = ({
|
|||||||
<AlertTriangle className="h-10 w-10 md:h-12 md:w-12 text-amber-500 mx-auto mb-2 md:mb-3 opacity-70" />
|
<AlertTriangle className="h-10 w-10 md:h-12 md:w-12 text-amber-500 mx-auto mb-2 md:mb-3 opacity-70" />
|
||||||
<h3 className="text-base md:text-lg font-medium mb-1">No uptime data available</h3>
|
<h3 className="text-base md:text-lg font-medium mb-1">No uptime data available</h3>
|
||||||
<p className="text-muted-foreground max-w-md text-sm md:text-base">
|
<p className="text-muted-foreground max-w-md text-sm md:text-base">
|
||||||
There's no monitoring data for this service in the selected time period.
|
There's no monitoring data for this service in the selected time period
|
||||||
|
{selectedRegionalAgent !== "default" ? " from the selected regional agent" : ""}.
|
||||||
This could be because the service was recently added or monitoring is paused.
|
This could be because the service was recently added or monitoring is paused.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ServiceNotificationFields } from "./add-service/ServiceNotificationFiel
|
|||||||
import { ServiceFormActions } from "./add-service/ServiceFormActions";
|
import { ServiceFormActions } from "./add-service/ServiceFormActions";
|
||||||
import { serviceService } from "@/services/serviceService";
|
import { serviceService } from "@/services/serviceService";
|
||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
|
import { ServiceRegionalFields } from "./add-service/ServiceRegionalFields";
|
||||||
|
|
||||||
interface ServiceFormProps {
|
interface ServiceFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@@ -43,6 +44,8 @@ export function ServiceForm({
|
|||||||
retries: "3",
|
retries: "3",
|
||||||
notificationChannel: "",
|
notificationChannel: "",
|
||||||
alertTemplate: "",
|
alertTemplate: "",
|
||||||
|
regionalMonitoringEnabled: false,
|
||||||
|
regionalAgent: "",
|
||||||
},
|
},
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
});
|
});
|
||||||
@@ -71,6 +74,11 @@ export function ServiceForm({
|
|||||||
urlValue = initialData.url || "";
|
urlValue = initialData.url || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle regional monitoring data - ensure proper assignment display
|
||||||
|
const regionalAgent = initialData.region_name && initialData.agent_id
|
||||||
|
? `${initialData.region_name}|${initialData.agent_id}`
|
||||||
|
: "";
|
||||||
|
|
||||||
// Reset the form with initial data values
|
// Reset the form with initial data values
|
||||||
form.reset({
|
form.reset({
|
||||||
name: initialData.name || "",
|
name: initialData.name || "",
|
||||||
@@ -81,10 +89,20 @@ export function ServiceForm({
|
|||||||
retries: String(initialData.retries || 3),
|
retries: String(initialData.retries || 3),
|
||||||
notificationChannel: initialData.notificationChannel === "none" ? "" : initialData.notificationChannel || "",
|
notificationChannel: initialData.notificationChannel === "none" ? "" : initialData.notificationChannel || "",
|
||||||
alertTemplate: initialData.alertTemplate === "default" ? "" : initialData.alertTemplate || "",
|
alertTemplate: initialData.alertTemplate === "default" ? "" : initialData.alertTemplate || "",
|
||||||
|
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||||
|
regionalAgent: regionalAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
console.log("Populating form with data:", { type: validType, url: urlValue, port: portValue });
|
console.log("Populating form with data:", {
|
||||||
|
type: validType,
|
||||||
|
url: urlValue,
|
||||||
|
port: portValue,
|
||||||
|
regionalAgent,
|
||||||
|
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||||
|
region_name: initialData.region_name,
|
||||||
|
agent_id: initialData.agent_id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [initialData, isEdit, form]);
|
}, [initialData, isEdit, form]);
|
||||||
|
|
||||||
@@ -97,6 +115,17 @@ export function ServiceForm({
|
|||||||
try {
|
try {
|
||||||
console.log("Form data being submitted:", data); // Debug log for submitted data
|
console.log("Form data being submitted:", data); // Debug log for submitted data
|
||||||
|
|
||||||
|
// Parse regional agent selection
|
||||||
|
let regionName = "";
|
||||||
|
let agentId = "";
|
||||||
|
|
||||||
|
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
|
||||||
|
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
|
||||||
|
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
||||||
|
regionName = parsedRegionName || "";
|
||||||
|
agentId = parsedAgentId || "";
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare service data with proper field mapping
|
// Prepare service data with proper field mapping
|
||||||
const serviceData = {
|
const serviceData = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -105,6 +134,10 @@ export function ServiceForm({
|
|||||||
retries: parseInt(data.retries),
|
retries: parseInt(data.retries),
|
||||||
notificationChannel: data.notificationChannel === "none" ? "" : data.notificationChannel,
|
notificationChannel: data.notificationChannel === "none" ? "" : data.notificationChannel,
|
||||||
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
||||||
|
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
|
||||||
|
// Always set region_name and agent_id - empty strings when unassigned
|
||||||
|
regionName: regionName,
|
||||||
|
agentId: agentId,
|
||||||
// Map the URL field to appropriate database field based on service type
|
// Map the URL field to appropriate database field based on service type
|
||||||
...(data.type === "dns"
|
...(data.type === "dns"
|
||||||
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
||||||
@@ -115,6 +148,8 @@ export function ServiceForm({
|
|||||||
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Service data being sent:", serviceData);
|
||||||
|
|
||||||
if (isEdit && initialData) {
|
if (isEdit && initialData) {
|
||||||
// Update existing service
|
// Update existing service
|
||||||
@@ -164,6 +199,11 @@ export function ServiceForm({
|
|||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
||||||
<ServiceConfigFields form={form} />
|
<ServiceConfigFields form={form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
|
||||||
|
<ServiceRegionalFields form={form} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusBadge } from "@/components/services/StatusBadge";
|
import { StatusBadge } from "@/components/services/StatusBadge";
|
||||||
import { ServiceMonitoringButton } from "@/components/services/ServiceMonitoringButton";
|
import { ServiceMonitoringButton } from "@/components/services/ServiceMonitoringButton";
|
||||||
|
import { RegionalAgentFilter } from "@/components/services/RegionalAgentFilter";
|
||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -11,9 +12,16 @@ import { cn } from "@/lib/utils";
|
|||||||
interface ServiceHeaderProps {
|
interface ServiceHeaderProps {
|
||||||
service: Service;
|
service: Service;
|
||||||
onStatusChange?: (newStatus: "up" | "down" | "paused" | "warning") => void;
|
onStatusChange?: (newStatus: "up" | "down" | "paused" | "warning") => void;
|
||||||
|
selectedRegionalAgent?: string;
|
||||||
|
onRegionalAgentChange?: (agent: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceHeader({ service, onStatusChange }: ServiceHeaderProps) {
|
export function ServiceHeader({
|
||||||
|
service,
|
||||||
|
onStatusChange,
|
||||||
|
selectedRegionalAgent,
|
||||||
|
onRegionalAgentChange
|
||||||
|
}: ServiceHeaderProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
@@ -71,6 +79,12 @@ export function ServiceHeader({ service, onStatusChange }: ServiceHeaderProps) {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<StatusBadge status={service.status} size="lg" />
|
<StatusBadge status={service.status} size="lg" />
|
||||||
|
{selectedRegionalAgent !== undefined && onRegionalAgentChange && (
|
||||||
|
<RegionalAgentFilter
|
||||||
|
selectedAgent={selectedRegionalAgent}
|
||||||
|
onAgentChange={onRegionalAgentChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ServiceMonitoringButton service={service} onStatusChange={onStatusChange} />
|
<ServiceMonitoringButton service={service} onStatusChange={onStatusChange} />
|
||||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
||||||
<span className="sr-only">More options</span>
|
<span className="sr-only">More options</span>
|
||||||
@@ -80,4 +94,4 @@ export function ServiceHeader({ service, onStatusChange }: ServiceHeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ export const ServiceHistoryDialog = ({
|
|||||||
{selectedService && (
|
{selectedService && (
|
||||||
<ServiceUptimeHistory
|
<ServiceUptimeHistory
|
||||||
serviceId={selectedService.id}
|
serviceId={selectedService.id}
|
||||||
|
serviceType={selectedService.type}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
/>
|
/>
|
||||||
@@ -71,4 +72,4 @@ export const ServiceHistoryDialog = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -13,6 +13,7 @@ import { ServiceNotificationFields } from "./ServiceNotificationFields";
|
|||||||
import { ServiceFormActions } from "./ServiceFormActions";
|
import { ServiceFormActions } from "./ServiceFormActions";
|
||||||
import { serviceService } from "@/services/serviceService";
|
import { serviceService } from "@/services/serviceService";
|
||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
|
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
||||||
|
|
||||||
interface ServiceFormProps {
|
interface ServiceFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@@ -44,6 +45,8 @@ export function ServiceForm({
|
|||||||
retries: "3",
|
retries: "3",
|
||||||
notificationChannel: "",
|
notificationChannel: "",
|
||||||
alertTemplate: "",
|
alertTemplate: "",
|
||||||
|
regionalMonitoringEnabled: false,
|
||||||
|
regionalAgent: "",
|
||||||
},
|
},
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
});
|
});
|
||||||
@@ -72,6 +75,11 @@ export function ServiceForm({
|
|||||||
urlValue = initialData.url || "";
|
urlValue = initialData.url || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle regional monitoring data - ensure proper assignment display
|
||||||
|
const regionalAgent = initialData.region_name && initialData.agent_id
|
||||||
|
? `${initialData.region_name}|${initialData.agent_id}`
|
||||||
|
: "";
|
||||||
|
|
||||||
// Reset the form with initial data values
|
// Reset the form with initial data values
|
||||||
form.reset({
|
form.reset({
|
||||||
name: initialData.name || "",
|
name: initialData.name || "",
|
||||||
@@ -82,10 +90,20 @@ export function ServiceForm({
|
|||||||
retries: String(initialData.retries || 3),
|
retries: String(initialData.retries || 3),
|
||||||
notificationChannel: initialData.notificationChannel || "",
|
notificationChannel: initialData.notificationChannel || "",
|
||||||
alertTemplate: initialData.alertTemplate || "",
|
alertTemplate: initialData.alertTemplate || "",
|
||||||
|
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||||
|
regionalAgent: regionalAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
console.log("Populating form with data:", { type: validType, url: urlValue, port: portValue });
|
console.log("Populating form with data:", {
|
||||||
|
type: validType,
|
||||||
|
url: urlValue,
|
||||||
|
port: portValue,
|
||||||
|
regionalAgent,
|
||||||
|
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||||
|
region_name: initialData.region_name,
|
||||||
|
agent_id: initialData.agent_id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [initialData, isEdit, form]);
|
}, [initialData, isEdit, form]);
|
||||||
|
|
||||||
@@ -98,6 +116,17 @@ export function ServiceForm({
|
|||||||
try {
|
try {
|
||||||
console.log("Form data being submitted:", data); // Debug log for submitted data
|
console.log("Form data being submitted:", data); // Debug log for submitted data
|
||||||
|
|
||||||
|
// Parse regional agent selection
|
||||||
|
let regionName = "";
|
||||||
|
let agentId = "";
|
||||||
|
|
||||||
|
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
|
||||||
|
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
|
||||||
|
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
||||||
|
regionName = parsedRegionName || "";
|
||||||
|
agentId = parsedAgentId || "";
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare service data with proper field mapping
|
// Prepare service data with proper field mapping
|
||||||
const serviceData = {
|
const serviceData = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -106,6 +135,10 @@ export function ServiceForm({
|
|||||||
retries: parseInt(data.retries),
|
retries: parseInt(data.retries),
|
||||||
notificationChannel: data.notificationChannel || undefined,
|
notificationChannel: data.notificationChannel || undefined,
|
||||||
alertTemplate: data.alertTemplate || undefined,
|
alertTemplate: data.alertTemplate || undefined,
|
||||||
|
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
|
||||||
|
// Always set region_name and agent_id - empty strings when unassigned
|
||||||
|
regionName: regionName,
|
||||||
|
agentId: agentId,
|
||||||
// Map the URL field to appropriate database field based on service type
|
// Map the URL field to appropriate database field based on service type
|
||||||
...(data.type === "dns"
|
...(data.type === "dns"
|
||||||
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
||||||
@@ -116,6 +149,8 @@ export function ServiceForm({
|
|||||||
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Service data being sent:", serviceData);
|
||||||
|
|
||||||
if (isEdit && initialData) {
|
if (isEdit && initialData) {
|
||||||
// Update existing service
|
// Update existing service
|
||||||
@@ -165,6 +200,11 @@ export function ServiceForm({
|
|||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
||||||
<ServiceConfigFields form={form} />
|
<ServiceConfigFields form={form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
|
||||||
|
<ServiceRegionalFields form={form} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
|
||||||
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { ServiceFormData } from "./types";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { regionalService } from "@/services/regionalService";
|
||||||
|
import { MapPin, Loader2, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface ServiceRegionalFieldsProps {
|
||||||
|
form: UseFormReturn<ServiceFormData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||||
|
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
||||||
|
const currentRegionalAgent = form.watch("regionalAgent");
|
||||||
|
|
||||||
|
const { data: regionalAgents = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['regional-services'],
|
||||||
|
queryFn: regionalService.getRegionalServices,
|
||||||
|
enabled: regionalMonitoringEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter only online agents
|
||||||
|
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
|
||||||
|
|
||||||
|
// Find the current agent name for display
|
||||||
|
const getCurrentAgentDisplay = () => {
|
||||||
|
if (!currentRegionalAgent || currentRegionalAgent === "unassign") {
|
||||||
|
return "Select a regional agent or unassign";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [regionName] = currentRegionalAgent.split("|");
|
||||||
|
const agent = onlineAgents.find(agent =>
|
||||||
|
`${agent.region_name}|${agent.agent_id}` === currentRegionalAgent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
return `${agent.region_name} (${agent.agent_ip_address})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If agent is not found in online agents, it might be offline but still assigned
|
||||||
|
return regionName || "Select a regional agent or unassign";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="regionalMonitoringEnabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base font-medium flex items-center gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
Regional Monitoring
|
||||||
|
</FormLabel>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Assign this service to a regional monitoring agent for distributed monitoring
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value || false}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{regionalMonitoringEnabled && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="regionalAgent"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Regional Agent</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
// Handle the unassign case by setting to empty string
|
||||||
|
field.onChange(value === "unassign" ? "" : value);
|
||||||
|
}}
|
||||||
|
value={field.value || "unassign"}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={
|
||||||
|
isLoading
|
||||||
|
? "Loading agents..."
|
||||||
|
: getCurrentAgentDisplay()
|
||||||
|
} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<SelectItem value="loading" disabled>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Loading agents...
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectItem value="unassign">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<X className="h-4 w-4 text-red-500" />
|
||||||
|
<span className="text-red-600">Unassign (No Regional Agent)</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
{onlineAgents.length === 0 ? (
|
||||||
|
<SelectItem value="no-agents" disabled>
|
||||||
|
No online regional agents available
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
onlineAgents.map((agent) => (
|
||||||
|
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="font-medium">{agent.region_name}</span>
|
||||||
|
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
||||||
|
<p className="text-sm text-amber-600">
|
||||||
|
No online regional agents found. Services will use default monitoring.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{currentRegionalAgent && currentRegionalAgent !== "" && currentRegionalAgent !== "unassign" && (
|
||||||
|
<p className="text-sm text-green-600">
|
||||||
|
Currently assigned to: {getCurrentAgentDisplay()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{(!currentRegionalAgent || currentRegionalAgent === "" || currentRegionalAgent === "unassign") && (
|
||||||
|
<p className="text-sm text-orange-600">
|
||||||
|
Service is unassigned and will use default monitoring.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ export const serviceSchema = z.object({
|
|||||||
retries: z.string(),
|
retries: z.string(),
|
||||||
notificationChannel: z.string().optional(),
|
notificationChannel: z.string().optional(),
|
||||||
alertTemplate: z.string().optional(),
|
alertTemplate: z.string().optional(),
|
||||||
|
// Regional monitoring fields
|
||||||
|
regionalMonitoringEnabled: z.boolean().optional(),
|
||||||
|
regionalAgent: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { uptimeService } from '@/services/uptimeService';
|
import { uptimeService } from '@/services/uptimeService';
|
||||||
@@ -14,16 +13,28 @@ interface UseUptimeDataProps {
|
|||||||
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
|
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
|
||||||
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
||||||
|
|
||||||
// Fetch real uptime history data if serviceId is provided
|
// Fetch real uptime history data if serviceId is provided - filtered for default monitoring with agent_id 1
|
||||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||||
queryKey: ['uptimeHistory', serviceId, serviceType],
|
queryKey: ['uptimeHistory', serviceId, serviceType],
|
||||||
queryFn: () => {
|
queryFn: async () => {
|
||||||
if (!serviceId) {
|
if (!serviceId) {
|
||||||
console.log('No serviceId provided, skipping fetch');
|
console.log('No serviceId provided, skipping fetch');
|
||||||
return Promise.resolve([]);
|
return [];
|
||||||
}
|
}
|
||||||
console.log(`Fetching uptime data for service ${serviceId} of type ${serviceType}`);
|
console.log(`Fetching uptime data for service ${serviceId} of type ${serviceType} - filtering for default monitoring with agent_id 1`);
|
||||||
return uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
|
|
||||||
|
// Get raw uptime history data
|
||||||
|
const rawData = await uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
|
||||||
|
|
||||||
|
// Filter for default monitoring records with agent_id 1 only
|
||||||
|
const filteredData = rawData.filter(record => {
|
||||||
|
// Keep records that either have no regional info (pure default) OR have agent_id 1
|
||||||
|
return (!record.region_name && !record.agent_id) ||
|
||||||
|
(record.agent_id === '1' || record.agent_id === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Filtered ${rawData.length} records to ${filteredData.length} records for default monitoring with agent_id 1`);
|
||||||
|
return filteredData;
|
||||||
},
|
},
|
||||||
enabled: !!serviceId,
|
enabled: !!serviceId,
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
@@ -37,7 +48,7 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
|
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
|
||||||
if (!data || data.length === 0) return [];
|
if (!data || data.length === 0) return [];
|
||||||
|
|
||||||
console.log(`Processing ${data.length} uptime records for service ${serviceId}`);
|
console.log(`Processing ${data.length} filtered uptime records for service ${serviceId}`);
|
||||||
|
|
||||||
// Sort data by timestamp (newest first)
|
// Sort data by timestamp (newest first)
|
||||||
const sortedData = [...data].sort((a, b) =>
|
const sortedData = [...data].sort((a, b) =>
|
||||||
@@ -47,7 +58,7 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
// Take the most recent 20 records to ensure we have enough data
|
// Take the most recent 20 records to ensure we have enough data
|
||||||
const recentData = sortedData.slice(0, 20);
|
const recentData = sortedData.slice(0, 20);
|
||||||
|
|
||||||
console.log(`Using ${recentData.length} most recent records`);
|
console.log(`Using ${recentData.length} most recent filtered records`);
|
||||||
|
|
||||||
return recentData;
|
return recentData;
|
||||||
};
|
};
|
||||||
@@ -55,12 +66,12 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
// Update history items when data changes
|
// Update history items when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uptimeData && uptimeData.length > 0) {
|
if (uptimeData && uptimeData.length > 0) {
|
||||||
console.log(`Received ${uptimeData.length} uptime records for service ${serviceId}`);
|
console.log(`Received ${uptimeData.length} filtered uptime records for service ${serviceId}`);
|
||||||
const processedData = processUptimeData(uptimeData, interval);
|
const processedData = processUptimeData(uptimeData, interval);
|
||||||
setHistoryItems(processedData);
|
setHistoryItems(processedData);
|
||||||
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
||||||
// Generate placeholder data when no real data is available
|
// Generate placeholder data when no real data is available
|
||||||
console.log(`No uptime data available for service ${serviceId}, generating placeholder`);
|
console.log(`No filtered uptime data available for service ${serviceId}, generating placeholder`);
|
||||||
|
|
||||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||||
? status
|
? status
|
||||||
@@ -68,8 +79,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
|
|
||||||
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
||||||
id: `placeholder-${serviceId}-${index}`,
|
id: `placeholder-${serviceId}-${index}`,
|
||||||
service_id: serviceId || "", // Include service_id
|
service_id: serviceId || "",
|
||||||
serviceId: serviceId || "", // Keep for backward compatibility
|
serviceId: serviceId || "",
|
||||||
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
||||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||||
responseTime: 0
|
responseTime: 0
|
||||||
@@ -97,8 +108,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: `padding-${serviceId}-${index}`,
|
id: `padding-${serviceId}-${index}`,
|
||||||
service_id: serviceId || "", // Include service_id
|
service_id: serviceId || "",
|
||||||
serviceId: serviceId || "", // Keep for backward compatibility
|
serviceId: serviceId || "",
|
||||||
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||||
status: lastStatus,
|
status: lastStatus,
|
||||||
responseTime: 0
|
responseTime: 0
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export const serviceService = {
|
|||||||
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
||||||
alerts: item.alerts || "unmuted", // Store actual database field
|
alerts: item.alerts || "unmuted", // Store actual database field
|
||||||
muteChangedAt: item.mute_changed_at,
|
muteChangedAt: item.mute_changed_at,
|
||||||
|
// Regional monitoring fields
|
||||||
|
region_name: item.region_name || "",
|
||||||
|
agent_id: item.agent_id || "",
|
||||||
|
regional_monitoring_enabled: item.regional_monitoring_enabled || false,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching services:", error);
|
console.error("Error fetching services:", error);
|
||||||
@@ -58,6 +62,10 @@ export const serviceService = {
|
|||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_id: params.notificationChannel,
|
notification_id: params.notificationChannel,
|
||||||
template_id: params.alertTemplate,
|
template_id: params.alertTemplate,
|
||||||
|
// Regional monitoring fields
|
||||||
|
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
|
||||||
|
region_name: params.regionName || "",
|
||||||
|
agent_id: params.agentId || "",
|
||||||
// Conditionally add fields based on service type
|
// Conditionally add fields based on service type
|
||||||
...(serviceType === "dns"
|
...(serviceType === "dns"
|
||||||
? { domain: params.domain, url: "", host: "", port: null } // DNS: store in domain field
|
? { domain: params.domain, url: "", host: "", port: null } // DNS: store in domain field
|
||||||
@@ -90,6 +98,9 @@ export const serviceService = {
|
|||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
|
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
|
||||||
|
region_name: record.region_name || "",
|
||||||
|
agent_id: record.agent_id || "",
|
||||||
} as Service;
|
} as Service;
|
||||||
|
|
||||||
// Immediately start monitoring for the new service
|
// Immediately start monitoring for the new service
|
||||||
@@ -117,6 +128,10 @@ export const serviceService = {
|
|||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_id: params.notificationChannel || null,
|
notification_id: params.notificationChannel || null,
|
||||||
template_id: params.alertTemplate || null,
|
template_id: params.alertTemplate || null,
|
||||||
|
// Regional monitoring fields
|
||||||
|
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
|
||||||
|
region_name: params.regionName || "",
|
||||||
|
agent_id: params.agentId || "",
|
||||||
// Conditionally update fields based on service type
|
// Conditionally update fields based on service type
|
||||||
...(serviceType === "dns"
|
...(serviceType === "dns"
|
||||||
? { domain: params.domain, url: "", host: "", port: null } // DNS: update domain field
|
? { domain: params.domain, url: "", host: "", port: null } // DNS: update domain field
|
||||||
@@ -156,6 +171,9 @@ export const serviceService = {
|
|||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
|
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
|
||||||
|
region_name: record.region_name || "",
|
||||||
|
agent_id: record.agent_id || "",
|
||||||
} as Service;
|
} as Service;
|
||||||
|
|
||||||
return updatedService;
|
return updatedService;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const uptimeService = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}`;
|
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_default`;
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
const cached = uptimeCache.get(cacheKey);
|
const cached = uptimeCache.get(cacheKey);
|
||||||
@@ -81,7 +81,7 @@ export const uptimeService = {
|
|||||||
|
|
||||||
// Determine the correct collection based on service type
|
// Determine the correct collection based on service type
|
||||||
const collection = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
|
const collection = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
|
||||||
console.log(`Fetching uptime history for service ${serviceId} from collection ${collection}, limit: ${limit}`);
|
console.log(`Fetching default uptime history for service ${serviceId} from collection ${collection}, limit: ${limit}`);
|
||||||
|
|
||||||
// Build filter to get records for specific service_id
|
// Build filter to get records for specific service_id
|
||||||
let filter = `service_id='${serviceId}'`;
|
let filter = `service_id='${serviceId}'`;
|
||||||
@@ -102,11 +102,11 @@ export const uptimeService = {
|
|||||||
$cancelKey: `uptime_history_${serviceId}_${Date.now()}`
|
$cancelKey: `uptime_history_${serviceId}_${Date.now()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Filter query: ${filter} on collection: ${collection}`);
|
console.log(`Filter query for default data: ${filter} on collection: ${collection}`);
|
||||||
|
|
||||||
const response = await pb.collection(collection).getList(1, limit, options);
|
const response = await pb.collection(collection).getList(1, limit, options);
|
||||||
|
|
||||||
console.log(`Fetched ${response.items.length} uptime records for service ${serviceId} from ${collection}`);
|
console.log(`Fetched ${response.items.length} records for service ${serviceId} from ${collection}`);
|
||||||
|
|
||||||
if (response.items.length > 0) {
|
if (response.items.length > 0) {
|
||||||
console.log(`Date range in results: ${response.items[response.items.length - 1].timestamp} to ${response.items[0].timestamp}`);
|
console.log(`Date range in results: ${response.items[response.items.length - 1].timestamp} to ${response.items[0].timestamp}`);
|
||||||
@@ -117,8 +117,8 @@ export const uptimeService = {
|
|||||||
// Transform the response items to UptimeData format
|
// Transform the response items to UptimeData format
|
||||||
const uptimeData = response.items.map(item => ({
|
const uptimeData = response.items.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
service_id: item.service_id, // Include service_id
|
service_id: item.service_id,
|
||||||
serviceId: item.service_id, // Keep for backward compatibility
|
serviceId: item.service_id,
|
||||||
timestamp: item.timestamp,
|
timestamp: item.timestamp,
|
||||||
status: item.status as "up" | "down" | "warning" | "paused",
|
status: item.status as "up" | "down" | "warning" | "paused",
|
||||||
responseTime: item.response_time || 0,
|
responseTime: item.response_time || 0,
|
||||||
@@ -140,7 +140,7 @@ export const uptimeService = {
|
|||||||
console.error(`Error fetching uptime history for service ${serviceId}:`, error);
|
console.error(`Error fetching uptime history for service ${serviceId}:`, error);
|
||||||
|
|
||||||
// Try to return cached data as fallback
|
// Try to return cached data as fallback
|
||||||
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}`;
|
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_default`;
|
||||||
const cached = uptimeCache.get(cacheKey);
|
const cached = uptimeCache.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`Using expired cached data for service ${serviceId} due to fetch error`);
|
console.log(`Using expired cached data for service ${serviceId} due to fetch error`);
|
||||||
@@ -151,5 +151,81 @@ export const uptimeService = {
|
|||||||
console.log(`Returning empty array for service ${serviceId} due to fetch error`);
|
console.log(`Returning empty array for service ${serviceId} due to fetch error`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUptimeHistoryByRegionalAgent(
|
||||||
|
serviceId: string,
|
||||||
|
limit: number = 50,
|
||||||
|
startDate?: Date,
|
||||||
|
endDate?: Date,
|
||||||
|
serviceType?: string,
|
||||||
|
regionName?: string,
|
||||||
|
agentId?: string
|
||||||
|
): Promise<UptimeData[]> {
|
||||||
|
try {
|
||||||
|
if (!regionName || !agentId) {
|
||||||
|
console.log('No region name or agent ID provided for regional query');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_${regionName}_${agentId}`;
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
const cached = uptimeCache.get(cacheKey);
|
||||||
|
if (cached && (Date.now() - cached.timestamp) < cached.expiresIn) {
|
||||||
|
console.log(`Using cached regional uptime history for service ${serviceId}`);
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the correct collection based on service type
|
||||||
|
const collection = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
|
||||||
|
console.log(`Fetching regional uptime history from collection: ${collection} for service: ${serviceId}, region: ${regionName}, agent: ${agentId}`);
|
||||||
|
|
||||||
|
// Build filter for regional agent data
|
||||||
|
let filter = `service_id="${serviceId}" && region_name="${regionName}" && agent_id="${agentId}"`;
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
const startISO = startDate.toISOString();
|
||||||
|
const endISO = endDate.toISOString();
|
||||||
|
filter += ` && timestamp>="${startISO}" && timestamp<="${endISO}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Regional filter query: ${filter} on collection: ${collection}`);
|
||||||
|
|
||||||
|
const records = await pb.collection(collection).getList(1, limit, {
|
||||||
|
sort: '-timestamp',
|
||||||
|
filter: filter,
|
||||||
|
$autoCancel: false,
|
||||||
|
$cancelKey: `regional_uptime_history_${serviceId}_${Date.now()}`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Retrieved ${records.items.length} regional uptime records from ${collection} for region ${regionName}, agent ${agentId}`);
|
||||||
|
|
||||||
|
const uptimeData = records.items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
service_id: item.service_id,
|
||||||
|
serviceId: item.service_id,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
status: item.status as "up" | "down" | "paused" | "warning",
|
||||||
|
responseTime: item.response_time || item.responseTime || 0,
|
||||||
|
error_message: item.error_message,
|
||||||
|
details: item.details,
|
||||||
|
created: item.created,
|
||||||
|
updated: item.updated
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
uptimeCache.set(cacheKey, {
|
||||||
|
data: uptimeData,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
expiresIn: CACHE_TTL
|
||||||
|
});
|
||||||
|
|
||||||
|
return uptimeData;
|
||||||
|
} catch (error) {
|
||||||
|
const collectionForError = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
|
||||||
|
console.error(`Error fetching regional uptime history from ${collectionForError}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export interface Service {
|
export interface Service {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,6 +31,10 @@ export interface Service {
|
|||||||
headers?: string;
|
headers?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
||||||
|
// Regional monitoring fields
|
||||||
|
region_name?: string;
|
||||||
|
agent_id?: string;
|
||||||
|
regional_monitoring_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateServiceParams {
|
export interface CreateServiceParams {
|
||||||
@@ -45,6 +48,10 @@ export interface CreateServiceParams {
|
|||||||
retries: number;
|
retries: number;
|
||||||
notificationChannel?: string;
|
notificationChannel?: string;
|
||||||
alertTemplate?: string;
|
alertTemplate?: string;
|
||||||
|
// Regional monitoring params
|
||||||
|
regionalMonitoringEnabled?: boolean;
|
||||||
|
regionName?: string;
|
||||||
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UptimeData {
|
export interface UptimeData {
|
||||||
@@ -60,6 +67,9 @@ export interface UptimeData {
|
|||||||
updated?: string;
|
updated?: string;
|
||||||
date?: string; // Keep for backward compatibility
|
date?: string; // Keep for backward compatibility
|
||||||
uptime?: number; // Keep for backward compatibility
|
uptime?: number; // Keep for backward compatibility
|
||||||
|
// Regional monitoring fields
|
||||||
|
region_name?: string;
|
||||||
|
agent_id?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PingData {
|
export interface PingData {
|
||||||
|
|||||||
Reference in New Issue
Block a user