From dd2c64420d1e2fc6c89ef46710d2063ea1a53952 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 6 Jun 2025 18:46:50 +0800 Subject: [PATCH] feat: Implement a modern and professional Operational Page --- .../operational-page/ComponentsSelector.tsx | 243 ++++++++++++ .../CreateOperationalPageDialog.tsx | 280 ++++++++++++++ .../EditOperationalPageDialog.tsx | 353 ++++++++++++++++++ .../operational-page/OperationalPageCard.tsx | 101 +++++ .../OperationalPageContent.tsx | 211 +++++++++++ .../operational-page/StatusBadge.tsx | 47 +++ 6 files changed, 1235 insertions(+) create mode 100644 application/src/components/operational-page/ComponentsSelector.tsx create mode 100644 application/src/components/operational-page/CreateOperationalPageDialog.tsx create mode 100644 application/src/components/operational-page/EditOperationalPageDialog.tsx create mode 100644 application/src/components/operational-page/OperationalPageCard.tsx create mode 100644 application/src/components/operational-page/OperationalPageContent.tsx create mode 100644 application/src/components/operational-page/StatusBadge.tsx diff --git a/application/src/components/operational-page/ComponentsSelector.tsx b/application/src/components/operational-page/ComponentsSelector.tsx new file mode 100644 index 0000000..e291a9f --- /dev/null +++ b/application/src/components/operational-page/ComponentsSelector.tsx @@ -0,0 +1,243 @@ +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react'; +import { StatusPageComponentRecord } from '@/types/statusPageComponents.types'; +import { useQuery } from '@tanstack/react-query'; +import { serviceService } from '@/services/serviceService'; + +interface ComponentsSelectorProps { + selectedComponents: Partial[]; + onComponentsChange: (components: Partial[]) => void; + onComponentDelete?: (componentId: string) => void; +} + +const componentTypes = [ + { value: 'uptime', label: 'Uptime Service', icon: Server }, + { value: 'ssl', label: 'SSL Certificate', icon: Shield }, + { value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle }, +]; + +export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => { + const [showAddForm, setShowAddForm] = useState(false); + const [newComponent, setNewComponent] = useState({ + name: '', + description: '', + service_id: '', + server_id: '', + display_order: selectedComponents.length + 1, + }); + + // Fetch uptime services for the dropdown + const { data: services = [] } = useQuery({ + queryKey: ['services'], + queryFn: serviceService.getServices, + }); + + const addComponent = () => { + if (!newComponent.name.trim()) return; + + const component: Partial = { + ...newComponent, + operational_status_id: '', // Will be set when page is created + }; + + onComponentsChange([...selectedComponents, component]); + setNewComponent({ + name: '', + description: '', + service_id: '', + server_id: '', + display_order: selectedComponents.length + 2, + }); + setShowAddForm(false); + }; + + const removeComponent = async (index: number) => { + const component = selectedComponents[index]; + + // If component has an ID, it exists in database and needs to be deleted + if (component.id && onComponentDelete) { + await onComponentDelete(component.id); + } else { + // For new components not yet saved, just remove from local state + const updated = selectedComponents.filter((_, i) => i !== index); + onComponentsChange(updated); + } + }; + + return ( + + + + + Status Page Components + + + Add monitoring components like uptime services, SSL certificates, and incident tracking + + + + {selectedComponents.length > 0 && ( +
+ +
+ {selectedComponents.map((component, index) => ( +
+
+
{component.name}
+ {component.description && ( +
{component.description}
+ )} +
+ {component.service_id && ( + + Service: {services.find(s => s.id === component.service_id)?.name || component.service_id} + + )} + {component.server_id && ( + + Server: {component.server_id} + + )} +
+
+ +
+ ))} +
+
+ )} + + {!showAddForm ? ( + + ) : ( +
+
+
+ + setNewComponent({ ...newComponent, name: e.target.value })} + /> +
+
+ + setNewComponent({ ...newComponent, display_order: parseInt(e.target.value) || 1 })} + /> +
+
+ +
+ +