พัฒนา Frontend เมนู สถานะระบบ (Health)

This commit is contained in:
Flook 2025-11-14 05:27:51 +07:00
parent a4b5dcf110
commit 02339b4fb6
7 changed files with 226 additions and 74 deletions

View 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;

View 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;

View 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,
};

View File

@ -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;

View 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>
);
}

View File

@ -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: '*',

View 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' }));
}
});
};