diff --git a/web/src/components/Health/InfraStatusCard.jsx b/web/src/components/Health/InfraStatusCard.jsx new file mode 100644 index 0000000..d4da7c8 --- /dev/null +++ b/web/src/components/Health/InfraStatusCard.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import TitleCard from '../TitleCard'; +import { FaDatabase, FaServer, FaFlask } from 'react-icons/fa'; +import ServiceStatus from '../ServiceStatus'; + +const infrastructureServicesMap = [ + { key: 'database', name: 'CockroachDB', icon: FaDatabase }, + { key: 'cache', name: 'Redis Cache', icon: FaServer }, + { key: 'storage', name: 'MinIO S3', icon: FaServer }, + { key: 'ai_service', name: 'MONAI FastAPI (Instance)', icon: FaFlask }, +]; + +const InfraStatusCard = ({ serviceData }) => { + return ( +
+

Infrastructure Status

+
+ {infrastructureServicesMap.map((service) => { + const svc = serviceData?.[service.key]; + if (!svc) return null; + return ( + + ); + })} +
+
+ ); +}; + +export default InfraStatusCard; \ No newline at end of file diff --git a/web/src/components/Health/ModelEndpointList.jsx b/web/src/components/Health/ModelEndpointList.jsx new file mode 100644 index 0000000..44a8c95 --- /dev/null +++ b/web/src/components/Health/ModelEndpointList.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { FaFlask, FaCheckCircle, FaTimesCircle } from 'react-icons/fa'; +import ServiceStatus from '../ServiceStatus'; + +const ModelEndpointsStatus = ({ modelData }) => { + const models = modelData.models || []; + + if (models.length === 0) { + return

ไม่พบ Model ที่มีสถานะ ACTIVE

; + } + + return ( +
+

+ + สถานะ Model Endpoints ({models.length} รายการ) +

+
+ {models.map((model) => ( + + ))} +
+
+ ); +}; + +export default ModelEndpointsStatus; \ No newline at end of file diff --git a/web/src/components/ServiceStatus.jsx b/web/src/components/ServiceStatus.jsx new file mode 100644 index 0000000..d05616a --- /dev/null +++ b/web/src/components/ServiceStatus.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { FaCheckCircle, FaTimesCircle, FaDatabase, FaServer, FaFlask } from 'react-icons/fa'; +import PropTypes from 'prop-types'; // สำหรับการตรวจสอบ Props (Best Practice) + +// Helper Function: Mapping Icon +const IconMap = { + FaDatabase, + FaServer, + FaFlask, + FaCheckCircle, + FaTimesCircle, +}; + +export default function ServiceStatus({ name, status, details, icon: Icon }) { + const isUp = status === 'UP' || status === 'Healthy'; + const colorClass = isUp ? 'text-success' : 'text-error'; + const badgeClass = isUp ? 'badge-success' : 'badge-error'; + + // Icon สำหรับแสดงสถานะ (ใช้ Check/Times Circle เป็น Fallback) + const StatusIcon = Icon || (isUp ? FaCheckCircle : FaTimesCircle); + + return ( +
+
+ {/* Icon ของ Service */} + + {/* ชื่อ Service */} + {name} +
+ +
+ {/* Badge แสดงสถานะ */} + + {/* รายละเอียด (Truncate เพื่อป้องกัน overflow) */} + {details} +
+
+ ); +} + +// กำหนด PropTypes เพื่อเพิ่มความแข็งแกร่งในการพัฒนา +ServiceStatus.propTypes = { + name: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + details: PropTypes.string.isRequired, + icon: PropTypes.elementType, +}; \ No newline at end of file diff --git a/web/src/config/sidebarRoutes.jsx b/web/src/config/sidebarRoutes.jsx index a70ff52..2bb6ae1 100644 --- a/web/src/config/sidebarRoutes.jsx +++ b/web/src/config/sidebarRoutes.jsx @@ -1,86 +1,31 @@ -import { FaTachometerAlt, FaCog, FaDatabase, FaCogs, FaProjectDiagram, FaFlask, - FaClipboardList, FaHeartbeat } from 'react-icons/fa'; - +import { FaTachometerAlt, FaCog, FaDatabase, FaHeartbeat, FaFlask } from 'react-icons/fa'; const routes = [ { path: '/dashboard', - icon: , + icon: , name: 'แดชบอร์ด/ภาพรวม', - // ไม่ต้องระบุ requiredRole (viewer/admin เข้าถึงได้เสมอ) }, // ----------------================-- - // กลุ่ม: Data & MLOps + // กลุ่ม: Data & AI/MLOps (เหลือแค่ Model Ops) // ---------------------------------- { path: '', - icon: , - name: 'ข้อมูล & AI/MLOps', // ปรับชื่อเล็กน้อย - // Roles: ใครก็ตามที่จัดการข้อมูล/โมเดล ควรเข้าถึงได้ - requiredRole: ['viewer', 'admin', 'manager'], - submenu: [ - // เพิ่ม Endpoint สำหรับเรียกใช้ AI Inference Proxy - { - path: - '/dashboard/inference-run', - name: 'AI Inference (Run)', // ชื่อสำหรับเรียกใช้ AI จริง - requiredRole: ['viewer', 'admin', 'manager'], // ผู้ใช้ทั่วไปก็ควรเรียกได้ - }, - { - path: - '/dashboard/model-registry', - name: 'Model Registry & Control', // รวม CRUD และ Control ไว้ในหน้าเดียว - // Roles: จำกัดเฉพาะผู้ที่มีสิทธิ์จัดการโมเดล (Admin/Manager) - requiredRole: ['manager', 'admin'], - }, - { - path: - '/dashboard/datasets', - name: 'Dataset Catalog (CKAN)', - requiredRole: ['viewer', 'admin', 'manager'], - }, - { - path: - '/dashboard/lineage', - name: 'Dataset Lineage', - requiredRole: ['viewer', 'admin', 'manager'], - }, - // Note: ลบ /dashboard/predict ออก เนื่องจาก /inference-run ควรจะครอบคลุม - ], - }, - - // ---------------------------------- - // กลุ่ม: Pipelines & Logs - // ---------------------------------- - { - path: '', - icon: , - name: 'Pipelines & Log', + icon: , + name: 'AI Model Operations', requiredRole: ['viewer', 'admin', 'manager'], submenu: [ { - path: - '/dashboard/pipeline/trigger', - name: 'Pipeline Control', - // Roles: จำกัดเฉพาะผู้ที่สั่งรันได้ (Admin/Manager) + path: '/dashboard/inference-run', + name: 'AI Inference (Run)', + requiredRole: ['viewer', 'admin', 'manager'], + }, + { + path: '/dashboard/model-registry', + name: 'Model Registry & Control', requiredRole: ['manager', 'admin'], }, - { - path: - '/dashboard/pipeline/logs', - name: 'Pipeline Logs', - requiredRole: ['viewer', 'admin', 'manager'], - }, - { - path: - '/dashboard/pipeline/status', - name: 'Pipeline Run Status', - requiredRole: ['viewer', 'admin', 'manager'], - }, ], }, @@ -89,20 +34,16 @@ const routes = [ // ---------------------------------- { path: '/dashboard/health', - icon: , + icon: , name: 'สถานะระบบ (Health)', requiredRole: ['viewer', 'admin', 'manager'], }, { path: '/dashboard/settings', - icon: , + icon: , name: 'ตั้งค่าระบบ', - requiredRole: ['admin'], // สำหรับ Superuser/Admin เท่านั้น + requiredRole: ['admin'], }, - ]; - export default routes; \ No newline at end of file diff --git a/web/src/pages/system/Health.jsx b/web/src/pages/system/Health.jsx new file mode 100644 index 0000000..b3a5e46 --- /dev/null +++ b/web/src/pages/system/Health.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import TitleCard from '../../components/TitleCard'; +import { useSystemHealth } from '../../services/healthApi'; +import { FaTimesCircle, FaSyncAlt, FaCheckCircle } from 'react-icons/fa'; + +// Imports Components ย่อย +import InfraStatusCard from '../../components/Health/InfraStatusCard'; +import ModelEndpointsStatus from '../../components/Health/ModelEndpointList'; + + +export default function SystemHealth() { + const { data, isLoading, isError, isFetching } = useSystemHealth(); + + // ตรวจสอบสถานะภาพรวม + const overallStatus = data?.status || (isError ? 'Error' : 'Loading'); + const overallColor = overallStatus === 'Healthy' ? 'alert-success' : overallStatus === 'Degraded' ? 'alert-warning' : 'alert-error'; + const lastChecked = data?.last_checked ? new Date(data.last_checked).toLocaleTimeString() : 'N/A'; + + if (isError) { + return +

ไม่สามารถเชื่อมต่อกับ Health API ได้ (Backend อาจหยุดทำงาน)

+
+ } + + return ( + : null} + > + + {/* 1. สถานะภาพรวม */} +
+ + {overallStatus === 'Healthy' ? : } + สถานะภาพรวม: {overallStatus} + +
+ + {/* 2. Infrastructure Services Card */} + {data?.services && } + + {/* 3. Model Endpoint List */} + {data?.services?.model_endpoints && } + + {/* เวลาตรวจสอบ */} +

+ {isLoading && !data ? "กำลังตรวจสอบสถานะครั้งแรก..." : `อัปเดตล่าสุด: ${lastChecked}`} +

+
+ ); +} \ No newline at end of file diff --git a/web/src/routes/pageRoutes.jsx b/web/src/routes/pageRoutes.jsx index d5a0b34..2650131 100644 --- a/web/src/routes/pageRoutes.jsx +++ b/web/src/routes/pageRoutes.jsx @@ -2,6 +2,7 @@ import Dashboard from '../pages/Dashboard'; import BlankPage from '../pages/BlankPage'; import ModelRegistry from '../pages/data/ModelRegistry'; import InferenceRun from '../pages/data/InferenceRun'; +import SystemHealth from '../pages/system/Health'; // Array ของเส้นทางย่อยภายใต้ /dashboard/ const pageRoutes = [ @@ -22,6 +23,12 @@ const pageRoutes = [ path: 'inference-run', element: , }, + // --- สถานะระบบ (Health) --- + { + // Path: /dashboard/health + path: 'health', + element: , + }, // Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ { path: '*', diff --git a/web/src/services/healthApi.js b/web/src/services/healthApi.js new file mode 100644 index 0000000..9cf5228 --- /dev/null +++ b/web/src/services/healthApi.js @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; +import axiosClient from './axiosClient'; +import { addToast } from '../features/toast/toastSlice'; +import { useDispatch } from 'react-redux'; + +const REFETCH_INTERVAL = 15000; // ดึงข้อมูลใหม่ทุก 15 วินาที + +/** + * Hook สำหรับดึงสถานะ Health Check ของระบบทั้งหมด + * Endpoint: GET /api/v1/health/ + */ +export const useSystemHealth = () => { + const dispatch = useDispatch(); + + return useQuery({ + queryKey: ['systemHealth'], + queryFn: async () => { + const response = await axiosClient.get('/api/v1/health/'); + + // แจ้งเตือนเมื่อสถานะกลับมา UP จาก Degraded + if (response.data.status === 'Healthy') { + // dispatch(addToast({ message: 'System Health Status: Healthy', type: 'info' })); + } + return response.data; + }, + // ตั้งค่า Polling ให้ดึงข้อมูลใหม่ทุก 15 วินาที + refetchInterval: REFETCH_INTERVAL, + staleTime: 5000, + + // จัดการ Error เมื่อไม่สามารถเชื่อมต่อ API ได้เลย (เช่น 500 หรือ Network Failure) + onError: () => { + dispatch(addToast({ message: 'Network Error: Could not reach Health API', type: 'error' })); + } + }); +}; \ No newline at end of file