พัฒนา Frontend เมนู สถานะระบบ (Health)
This commit is contained in:
parent
a4b5dcf110
commit
02339b4fb6
36
web/src/components/Health/InfraStatusCard.jsx
Normal file
36
web/src/components/Health/InfraStatusCard.jsx
Normal file
@ -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 (
|
||||
<div className="card bg-base-200 shadow-md">
|
||||
<h3 className="card-title p-4 pb-0 text-base-content">Infrastructure Status</h3>
|
||||
<div className="card-body p-0 divide-y divide-base-300">
|
||||
{infrastructureServicesMap.map((service) => {
|
||||
const svc = serviceData?.[service.key];
|
||||
if (!svc) return null;
|
||||
return (
|
||||
<ServiceStatus
|
||||
key={service.key}
|
||||
name={service.name}
|
||||
status={svc.status}
|
||||
details={svc.details}
|
||||
icon={service.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfraStatusCard;
|
||||
34
web/src/components/Health/ModelEndpointList.jsx
Normal file
34
web/src/components/Health/ModelEndpointList.jsx
Normal file
@ -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 <p className="text-center text-gray-500 p-4">ไม่พบ Model ที่มีสถานะ ACTIVE</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-semibold mb-3 flex items-center space-x-2 text-base-content">
|
||||
<FaFlask className="text-primary" />
|
||||
<span>สถานะ Model Endpoints ({models.length} รายการ)</span>
|
||||
</h3>
|
||||
<div className="bg-base-100 shadow-inner rounded-lg border border-base-300 divide-y divide-base-200">
|
||||
{models.map((model) => (
|
||||
<ServiceStatus
|
||||
key={model.id}
|
||||
name={`${model.name} (v${model.model_version})`}
|
||||
status={model.status}
|
||||
// แสดง Latency หรือ Detail ของ Check
|
||||
details={model.details}
|
||||
icon={model.status === 'UP' ? FaCheckCircle : FaTimesCircle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelEndpointsStatus;
|
||||
47
web/src/components/ServiceStatus.jsx
Normal file
47
web/src/components/ServiceStatus.jsx
Normal file
@ -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 (
|
||||
<div className="flex items-center justify-between p-3 hover:bg-base-300 transition duration-150 last:border-b-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Icon ของ Service */}
|
||||
<StatusIcon className={`w-5 h-5 ${colorClass}`} />
|
||||
{/* ชื่อ Service */}
|
||||
<span className="font-semibold text-base-content">{name}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-x-3 text-right">
|
||||
{/* Badge แสดงสถานะ */}
|
||||
<span className={`badge ${badgeClass} badge-outline text-xs hidden sm:inline`}>{status}</span>
|
||||
{/* รายละเอียด (Truncate เพื่อป้องกัน overflow) */}
|
||||
<span className="text-xs text-gray-500 max-w-[200px] inline-block truncate" title={details}>{details}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// กำหนด PropTypes เพื่อเพิ่มความแข็งแกร่งในการพัฒนา
|
||||
ServiceStatus.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
details: PropTypes.string.isRequired,
|
||||
icon: PropTypes.elementType,
|
||||
};
|
||||
@ -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: <FaTachometerAlt
|
||||
className="w-5 h-5 flex-shrink-0" />,
|
||||
icon: <FaTachometerAlt className="w-5 h-5 flex-shrink-0" />,
|
||||
name: 'แดชบอร์ด/ภาพรวม',
|
||||
// ไม่ต้องระบุ requiredRole (viewer/admin เข้าถึงได้เสมอ)
|
||||
},
|
||||
|
||||
// ----------------================--
|
||||
// กลุ่ม: Data & MLOps
|
||||
// กลุ่ม: Data & AI/MLOps (เหลือแค่ Model Ops)
|
||||
// ----------------------------------
|
||||
{
|
||||
path: '',
|
||||
icon: <FaDatabase
|
||||
className="w-5 h-5 flex-shrink-0" />,
|
||||
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: <FaProjectDiagram
|
||||
className="w-5 h-5 flex-shrink-0" />,
|
||||
name: 'Pipelines & Log',
|
||||
icon: <FaFlask className="w-5 h-5 flex-shrink-0" />,
|
||||
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: <FaHeartbeat
|
||||
className="w-5 h-5 flex-shrink-0" />,
|
||||
icon: <FaHeartbeat className="w-5 h-5 flex-shrink-0" />,
|
||||
name: 'สถานะระบบ (Health)',
|
||||
requiredRole: ['viewer', 'admin', 'manager'],
|
||||
},
|
||||
{
|
||||
path: '/dashboard/settings',
|
||||
icon: <FaCog
|
||||
className="w-5 h-5 flex-shrink-0" />,
|
||||
icon: <FaCog className="w-5 h-5 flex-shrink-0" />,
|
||||
name: 'ตั้งค่าระบบ',
|
||||
requiredRole: ['admin'], // สำหรับ Superuser/Admin เท่านั้น
|
||||
requiredRole: ['admin'],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
|
||||
export default routes;
|
||||
52
web/src/pages/system/Health.jsx
Normal file
52
web/src/pages/system/Health.jsx
Normal file
@ -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 <TitleCard title="สถานะระบบ" topMargin="mt-0">
|
||||
<p className="text-error p-4">ไม่สามารถเชื่อมต่อกับ Health API ได้ (Backend อาจหยุดทำงาน)</p>
|
||||
</TitleCard>
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleCard
|
||||
title="สถานะระบบ (Health Check)"
|
||||
topMargin="mt-0"
|
||||
TopSideButtons={isFetching ? <FaSyncAlt className="animate-spin text-primary" title="Refreshing..." /> : null}
|
||||
>
|
||||
|
||||
{/* 1. สถานะภาพรวม */}
|
||||
<div className={`alert ${overallColor} text-base-content mb-6 shadow-lg`}>
|
||||
<span className="font-bold text-lg flex items-center space-x-3">
|
||||
{overallStatus === 'Healthy' ? <FaCheckCircle /> : <FaTimesCircle />}
|
||||
<span>สถานะภาพรวม: {overallStatus}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 2. Infrastructure Services Card */}
|
||||
{data?.services && <InfraStatusCard serviceData={data.services} />}
|
||||
|
||||
{/* 3. Model Endpoint List */}
|
||||
{data?.services?.model_endpoints && <ModelEndpointsStatus modelData={data.services.model_endpoints} />}
|
||||
|
||||
{/* เวลาตรวจสอบ */}
|
||||
<p className="text-xs text-gray-500 mt-6">
|
||||
{isLoading && !data ? "กำลังตรวจสอบสถานะครั้งแรก..." : `อัปเดตล่าสุด: ${lastChecked}`}
|
||||
</p>
|
||||
</TitleCard>
|
||||
);
|
||||
}
|
||||
@ -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: <InferenceRun />,
|
||||
},
|
||||
// --- สถานะระบบ (Health) ---
|
||||
{
|
||||
// Path: /dashboard/health
|
||||
path: 'health',
|
||||
element: <SystemHealth />,
|
||||
},
|
||||
// Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ
|
||||
{
|
||||
path: '*',
|
||||
|
||||
35
web/src/services/healthApi.js
Normal file
35
web/src/services/healthApi.js
Normal file
@ -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' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user