เพิ่ม Model Registry
This commit is contained in:
parent
66b95f6475
commit
119cf91905
40
web/package-lock.json
generated
40
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"axios": "^1.13.2",
|
||||
"daisyui": "^5.3.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.66.0",
|
||||
@ -2784,7 +2785,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@ -3143,6 +3143,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@ -3244,6 +3256,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@ -3383,6 +3404,17 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
@ -3451,6 +3483,12 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"axios": "^1.13.2",
|
||||
"daisyui": "^5.3.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.66.0",
|
||||
|
||||
189
web/src/components/ModalForm.jsx
Normal file
189
web/src/components/ModalForm.jsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import InputText from './InputText';
|
||||
import { modelSchema } from '../schemas/modelSchema';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
export default function ModalForm({ isOpen, onClose, mode, OnSubmit, model }) {
|
||||
|
||||
// สถานะ Status Choices จาก AiModel (กำหนดเองใน Frontend)
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'ACTIVE', name: 'ACTIVE (พร้อมใช้งาน)' },
|
||||
{ value: 'INACTIVE', name: 'INACTIVE (ไม่ได้ใช้งาน)' },
|
||||
{ value: 'TESTING', name: 'TESTING (กำลังทดสอบ)' },
|
||||
];
|
||||
|
||||
// 1. RHF Setup
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(modelSchema), // ใช้ Schema สำหรับ Model Registry
|
||||
// กำหนดค่าเริ่มต้นตาม mode
|
||||
defaultValues: {
|
||||
id: '',
|
||||
name: '',
|
||||
model_version: 'v1.0.0',
|
||||
developer: '',
|
||||
base_url: '',
|
||||
inference_path: '',
|
||||
status: 'INACTIVE',
|
||||
auth_required: false,
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Logic โหลดข้อมูล Model เข้า Form เมื่อเปิดโหมด Edit
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && model) {
|
||||
// ใช้ reset() ของ RHF เพื่อโหลดข้อมูลลงในฟอร์ม
|
||||
reset({
|
||||
id: model.id || '', // ID เป็น ReadOnly
|
||||
name: model.name || '',
|
||||
model_version: model.model_version || 'v1.0.0',
|
||||
developer: model.developer || '',
|
||||
base_url: model.base_url || '',
|
||||
inference_path: model.inference_path || '',
|
||||
status: model.status || 'INACTIVE',
|
||||
auth_required: model.auth_required || false,
|
||||
});
|
||||
} else {
|
||||
// Reset ฟอร์มเป็นค่าเริ่มต้นเมื่อเปิดโหมด Add
|
||||
reset();
|
||||
}
|
||||
}, [mode, model, reset]);
|
||||
|
||||
|
||||
// 3. Logic การ Submit
|
||||
const onSubmitHandler = (data) => {
|
||||
// เพิ่ม ID เข้าไปในข้อมูลถ้าเป็นโหมด Edit
|
||||
if (mode === 'edit') {
|
||||
data.id = model.id;
|
||||
}
|
||||
|
||||
// เรียกใช้ OnSubmit ที่มาจาก Page Component (เรียก Mutation)
|
||||
OnSubmit(data);
|
||||
|
||||
onClose(); // ปิด Modal หลังจาก Submit
|
||||
};
|
||||
|
||||
const modalTitle = mode === 'add' ? 'ลงทะเบียน AI Model ใหม่' : `แก้ไข Model: ${model?.name}`;
|
||||
const submitText = mode === 'add' ? 'ลงทะเบียน' : 'บันทึกการแก้ไข';
|
||||
|
||||
|
||||
return (
|
||||
<dialog id="model_form_modal" className="modal" open={isOpen}>
|
||||
<div className="modal-box w-11/12 max-w-4xl"> {/* เพิ่มขนาด Modal */}
|
||||
<h3 className="font-bold text-xl py-4">{modalTitle}</h3>
|
||||
|
||||
{/* ปุ่มปิด Modal */}
|
||||
<button type="button" className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={onClose}>✕</button>
|
||||
|
||||
{/* 4. Form หลัก (ใช้ handleSubmit ของ RHF) */}
|
||||
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* ---------------------------------- */}
|
||||
{/* กลุ่มที่ 1: ชื่อและสถานะ */}
|
||||
{/* ---------------------------------- */}
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="w-full">
|
||||
<InputText
|
||||
labelTitle="ชื่อโมเดล (เช่น Spleen Segmentation)"
|
||||
placeholder="Model Name"
|
||||
type="text"
|
||||
{...register('name')}
|
||||
error={errors.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InputText
|
||||
labelTitle="เวอร์ชัน"
|
||||
placeholder="v1.0.0"
|
||||
type="text"
|
||||
{...register('model_version')}
|
||||
error={errors.model_version}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---------------------------------- */}
|
||||
{/* กลุ่มที่ 2: Base URL / Inference Path */}
|
||||
{/* ---------------------------------- */}
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="w-full md:w-3/5">
|
||||
<InputText
|
||||
labelTitle="Base URL (Internal Service Address)"
|
||||
placeholder="http://ai_model_server:8001"
|
||||
type="url"
|
||||
{...register('base_url')}
|
||||
error={errors.base_url}
|
||||
labelStyle='text-warning'
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-2/5">
|
||||
<InputText
|
||||
labelTitle="Inference Path"
|
||||
placeholder="inference/spleen/"
|
||||
type="text"
|
||||
{...register('inference_path')}
|
||||
error={errors.inference_path}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---------------------------------- */}
|
||||
{/* กลุ่มที่ 3: สถานะและการควบคุม */}
|
||||
{/* ---------------------------------- */}
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="w-full md:w-1/2">
|
||||
<SelectInput
|
||||
labelTitle="สถานะบริการ"
|
||||
options={STATUS_OPTIONS}
|
||||
{...register('status')} // RHF register
|
||||
error={errors.status} // RHF error
|
||||
/>
|
||||
{errors.status && <p className="text-error text-xs mt-1">{errors.status.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/2 flex items-end">
|
||||
<div className="form-control">
|
||||
<label className="label cursor-pointer space-x-2">
|
||||
<span className="label-text">ต้องการ Internal Auth Key?</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
{...register('auth_required')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<InputText
|
||||
labelTitle="ผู้พัฒนา/ทีม"
|
||||
placeholder="Core AI Team"
|
||||
type="text"
|
||||
{...register('developer')}
|
||||
error={errors.developer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 5. ปุ่ม Submit และ Cancel */}
|
||||
<div className="modal-action mt-6">
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose} disabled={isSubmitting}>ยกเลิก</button>
|
||||
<button type="submit" className={"btn btn-success" + (isSubmitting ? " loading" : "")} disabled={isSubmitting}>
|
||||
{submitText}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
113
web/src/components/ModelRegistry/ModelTable.jsx
Normal file
113
web/src/components/ModelRegistry/ModelTable.jsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { useTestConnection } from '../../services/modelRegistryApi'; // Hook สำหรับ Test Connection
|
||||
import { FaCheckCircle, FaExclamationTriangle, FaTrash, FaEdit } from 'react-icons/fa';
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Helper Function: แสดง Badge สถานะ
|
||||
// ----------------------------------------------------
|
||||
const getStatusBadge = (status) => {
|
||||
const baseClass = "badge badge-sm";
|
||||
switch (status) {
|
||||
case 'ACTIVE': return <div className={`${baseClass} badge-success font-medium`}>ACTIVE</div>;
|
||||
case 'TESTING': return <div className={`${baseClass} badge-warning font-medium`}>TESTING</div>;
|
||||
case 'INACTIVE': return <div className={`${baseClass} badge-info font-medium`}>INACTIVE</div>;
|
||||
default: return <div className={`${baseClass} badge-neutral font-medium`}>N/A</div>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Component สำหรับแสดงตาราง Model Registry
|
||||
* @param {Array} models - ข้อมูล Models ที่กรองแล้ว (จาก useModelList)
|
||||
* @param {function} handleOpenEdit - Handler สำหรับเปิด Modal แก้ไข
|
||||
* @param {function} handleDelete - Handler สำหรับลบ Model
|
||||
* @param {boolean} deleteLoading - สถานะ Loading ของ Delete Mutation
|
||||
*/
|
||||
function ModelTable({ models, handleOpenEdit, handleDelete, deleteLoading }) {
|
||||
// 1. Hook สำหรับทดสอบการเชื่อมต่อ (Test Connection)
|
||||
// ใช้ Hook ตรงนี้ เพราะเป็น Logic ที่เกี่ยวข้องกับ Action ในตารางโดยตรง
|
||||
const testConnectionMutation = useTestConnection();
|
||||
|
||||
const handleTest = (modelId) => {
|
||||
// เรียกใช้ Mutation เพื่อทดสอบ
|
||||
testConnectionMutation.mutate(modelId, {
|
||||
onSuccess: (result) => {
|
||||
const status = result.status === 'success' ? 'SUCCESS' : 'FAILED';
|
||||
alert(`Test Result: ${status}\nMessage: ${result.message || JSON.stringify(result.response_data)}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`Test Failed! Error: ${error.message || 'Check network or base URL'}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 2. Logic การแสดงผลตาราง
|
||||
return (
|
||||
<table className="table w-full table-zebra table-pin-rows">
|
||||
{/* Table Head */}
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='w-1/6'>Model/Version</th>
|
||||
<th className='w-1/4'>Base URL</th>
|
||||
<th>Developer</th>
|
||||
<th className='w-1/12'>Status</th>
|
||||
<th className='w-1/6'>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Table Body */}
|
||||
<tbody>
|
||||
{models.map((model) => (
|
||||
<tr className="hover:bg-base-300" key={model.id}>
|
||||
<td className='font-semibold'>
|
||||
{model.name}
|
||||
<span className="badge badge-xs badge-ghost ml-2">{model.model_version}</span>
|
||||
</td>
|
||||
<td className='text-xs max-w-xs overflow-hidden truncate tooltip' data-tip={model.full_endpoint}>
|
||||
{model.full_endpoint}
|
||||
</td>
|
||||
<td>{model.developer || 'N/A'}</td>
|
||||
<td>{getStatusBadge(model.status)}</td>
|
||||
|
||||
{/* Actions (CRUD/Control) */}
|
||||
<td className='flex space-x-1'>
|
||||
{/* Edit/Update Button (Protected by RBAC in Backend) */}
|
||||
<button
|
||||
className="btn btn-outline btn-secondary btn-xs tooltip"
|
||||
data-tip="Edit Model Metadata"
|
||||
onClick={() => handleOpenEdit(model)}
|
||||
>
|
||||
<FaEdit className="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
{/* Test Connection Button */}
|
||||
<button
|
||||
className="btn btn-outline btn-info btn-xs tooltip"
|
||||
data-tip="Test Connection"
|
||||
onClick={() => handleTest(model.id)}
|
||||
disabled={testConnectionMutation.isPending}
|
||||
>
|
||||
{testConnectionMutation.isPending ?
|
||||
<span className="loading loading-spinner loading-xs"></span> :
|
||||
<FaCheckCircle className="w-3 h-3" />
|
||||
}
|
||||
</button>
|
||||
|
||||
{/* Delete Button (Protected by RBAC in Backend) */}
|
||||
<button
|
||||
className="btn btn-outline btn-error btn-xs tooltip"
|
||||
data-tip="Delete Model"
|
||||
onClick={() => handleDelete(model.id)}
|
||||
disabled={deleteLoading || testConnectionMutation.isPending}
|
||||
>
|
||||
<FaTrash className="w-3 h-3" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModelTable;
|
||||
26
web/src/components/ModelRegistry/ModelTopBar.jsx
Normal file
26
web/src/components/ModelRegistry/ModelTopBar.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { FaPlus, FaSearch } from 'react-icons/fa';
|
||||
|
||||
function ModelTopBar({ onOpenAdd, onSearch }) {
|
||||
return (
|
||||
<div className='flex items-center space-x-3'>
|
||||
{/* Search Bar */}
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
className="input input-bordered input-sm w-32 md:w-auto"
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-square btn-sm"><FaSearch /></button>
|
||||
</div>
|
||||
|
||||
{/* Add Button */}
|
||||
<button className="btn btn-primary btn-sm" onClick={onOpenAdd}>
|
||||
<FaPlus className="w-4 h-4" /> Register New Model
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModelTopBar;
|
||||
48
web/src/components/SelectInput.jsx
Normal file
48
web/src/components/SelectInput.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// 1. สร้าง Functional Component ปกติ (ไม่ใช้ forwardRef โดยตรง)
|
||||
function SelectInputBase({ labelTitle, options, containerStyle, error, ...rest }, ref) {
|
||||
return (
|
||||
<div className={`form-control w-full ${containerStyle || ""}`}>
|
||||
<label className="label">
|
||||
<span className="label-text text-base-content">{labelTitle}</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={`select select-bordered w-full ${error ? "select-error" : ""}`}
|
||||
>
|
||||
<option value="" disabled>
|
||||
--- กรุณาเลือก ---
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{error && <p className="text-error text-xs mt-1">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. กำหนด propTypes ปกติ (จะไม่มี warning อีก)
|
||||
SelectInputBase.propTypes = {
|
||||
labelTitle: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
containerStyle: PropTypes.string,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
// 3. ใช้ forwardRef กับตัวหลัก
|
||||
const SelectInput = forwardRef(SelectInputBase);
|
||||
|
||||
export default SelectInput;
|
||||
104
web/src/pages/data/ModelRegistry.jsx
Normal file
104
web/src/pages/data/ModelRegistry.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import TitleCard from '../../components/TitleCard';
|
||||
import ModalForm from '../../components/ModalForm';
|
||||
|
||||
// Imports สำหรับ Hook API และ Components ย่อย
|
||||
import {
|
||||
useModelList,
|
||||
useTestConnection,
|
||||
useDeleteModel,
|
||||
useCreateModel,
|
||||
useUpdateModel,
|
||||
} from '../../services/modelRegistryApi';
|
||||
import ModelTable from '../../components/ModelRegistry/ModelTable';
|
||||
import ModelTopBar from '../../components/ModelRegistry/ModelTopBar';
|
||||
|
||||
|
||||
function ModelRegistry() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [modalMode, setModalMode] = useState("add");
|
||||
const [selectedModel, setSelectedModel] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState(''); // State สำหรับ Search
|
||||
|
||||
// TanStack Query Hooks
|
||||
const { data: models, isLoading, isError } = useModelList();
|
||||
const deleteMutation = useDeleteModel();
|
||||
const createMutation = useCreateModel();
|
||||
const updateMutation = useUpdateModel();
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Logic การจัดการ
|
||||
// ----------------------------------------------------
|
||||
const handleOpen = (mode, model = null) => {
|
||||
setIsOpen(true);
|
||||
setModalMode(mode);
|
||||
setSelectedModel(model);
|
||||
};
|
||||
|
||||
const handleSubmit = (newModelData) => {
|
||||
if (modalMode === 'add') {
|
||||
createMutation.mutate(newModelData);
|
||||
} else if (modalMode === 'edit') {
|
||||
updateMutation.mutate({ id: newModelData.id, data: newModelData });
|
||||
}
|
||||
// ModalForm.jsx ควรจัดการ onClose() เองเมื่อ Submit สำเร็จ
|
||||
};
|
||||
|
||||
const handleDelete = (modelId) => {
|
||||
if (confirm("คุณแน่ใจหรือไม่ที่จะลบ Model ID: " + modelId + " นี้?")) {
|
||||
deleteMutation.mutate(modelId);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Logic การกรองข้อมูล (Filter/Search)
|
||||
// ----------------------------------------------------
|
||||
const filteredModels = models?.filter(model =>
|
||||
model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
model.model_version.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
model.developer?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const isSubmitting = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
|
||||
if (isLoading) return <p className="text-center p-8">กำลังโหลดรายการ Model Registry...</p>;
|
||||
if (isError) return <p className="text-center p-8 text-error">ไม่สามารถดึงข้อมูล Model Registry ได้</p>;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-0">
|
||||
<TitleCard
|
||||
title="AI Model Registry & Control"
|
||||
topMargin="mt-0"
|
||||
TopSideButtons={<ModelTopBar onOpenAdd={() => handleOpen('add')} onSearch={setSearchTerm} />}
|
||||
>
|
||||
<div className="overflow-x-auto w-full">
|
||||
{/* ส่ง useTestConnection ลงไปใน ModelTable เพื่อใช้จริง */}
|
||||
<ModelTable
|
||||
models={filteredModels || []}
|
||||
handleOpenEdit={(model) => handleOpen('edit', model)}
|
||||
handleDelete={handleDelete}
|
||||
deleteLoading={deleteMutation.isPending}
|
||||
|
||||
// ส่ง Hook ไปให้ Component ย่อยใช้งาน
|
||||
useTestConnectionHook={useTestConnection}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-4 text-xs text-base-content/70'>Total Models: {filteredModels?.length || 0}</div>
|
||||
</TitleCard>
|
||||
|
||||
{/* Modal Form */}
|
||||
<ModalForm
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
OnSubmit={handleSubmit}
|
||||
mode={modalMode}
|
||||
model={selectedModel}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModelRegistry;
|
||||
@ -1,5 +1,6 @@
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import BlankPage from '../pages/BlankPage';
|
||||
import ModelRegistry from '../pages/data/ModelRegistry';
|
||||
|
||||
// Array ของเส้นทางย่อยภายใต้ /dashboard/
|
||||
const pageRoutes = [
|
||||
@ -8,6 +9,12 @@ const pageRoutes = [
|
||||
path: '', // ตรงกับ /dashboard
|
||||
element: <Dashboard />,
|
||||
},
|
||||
// --- Model Registry & Control ---
|
||||
{
|
||||
// Path: /dashboard/model-registry
|
||||
path: 'model-registry',
|
||||
element: <ModelRegistry />,
|
||||
},
|
||||
// Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ
|
||||
{
|
||||
path: '*',
|
||||
|
||||
11
web/src/schemas/modelSchema.js
Normal file
11
web/src/schemas/modelSchema.js
Normal file
@ -0,0 +1,11 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const modelSchema = yup.object().shape({
|
||||
name: yup.string().required('ต้องระบุชื่อโมเดล'),
|
||||
model_version: yup.string().required('ต้องระบุเวอร์ชัน'),
|
||||
developer: yup.string().nullable(),
|
||||
base_url: yup.string().url('ต้องเป็นรูปแบบ URL ที่ถูกต้อง').required('ต้องระบุ Internal Base URL'),
|
||||
inference_path: yup.string().required('ต้องระบุ Inference Path'),
|
||||
status: yup.string().oneOf(['ACTIVE', 'INACTIVE', 'TESTING']).required(),
|
||||
auth_required: yup.boolean(),
|
||||
});
|
||||
108
web/src/services/modelRegistryApi.js
Normal file
108
web/src/services/modelRegistryApi.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import axiosClient from './axiosClient'; // Axios Client ที่มี JWT Interceptor
|
||||
|
||||
const STALE_TIME = 60 * 1000; // 1 นาที
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Model Registry Queries (GET)
|
||||
// ----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook สำหรับดึงรายการ AI Model ทั้งหมด
|
||||
* Endpoint: GET /api/v1/models/ (Protected by IsAuthenticated)
|
||||
*/
|
||||
export const useModelList = () => {
|
||||
return useQuery({
|
||||
queryKey: ['modelList'],
|
||||
queryFn: async () => {
|
||||
const response = await axiosClient.get('/api/v1/models/');
|
||||
return response.data; // คาดหวัง Array ของ Model Objects
|
||||
},
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Model Control Mutations (POST, PATCH, DELETE)
|
||||
// ----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook สำหรับลงทะเบียน Model ใหม่ (POST)
|
||||
* Endpoint: POST /api/v1/models/ (Protected by IsAdminOrManager)
|
||||
*/
|
||||
export const useCreateModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (modelData) => {
|
||||
// modelData: { name, model_version, base_url, inference_path, ... }
|
||||
const response = await axiosClient.post('/api/v1/models/', modelData);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกลงทะเบียนสำเร็จแล้ว!');
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การลงทะเบียนล้มเหลว: ${error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล'}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook สำหรับลบ Model (DELETE)
|
||||
* Endpoint: DELETE /api/v1/models/{id}/ (Protected by IsAdminOrManager)
|
||||
*/
|
||||
export const useDeleteModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (modelId) => {
|
||||
const response = await axiosClient.delete(`/api/v1/models/${modelId}/`);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกลบสำเร็จแล้ว!');
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การลบล้มเหลว: ${error.response?.data?.detail || 'คุณอาจไม่มีสิทธิ์'}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook สำหรับทดสอบการเชื่อมต่อ (POST /test-connection/)
|
||||
* Endpoint: POST /api/v1/models/{id}/test-connection/ (Protected by IsAuthenticated)
|
||||
*/
|
||||
export const useTestConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (modelId) => {
|
||||
// Note: Endpoints นี้ต้องการ Body แต่ถ้า Backend ไม่ได้ใช้ เราส่ง Body เปล่า
|
||||
const response = await axiosClient.post(`/api/v1/models/${modelId}/test-connection/`, {});
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook สำหรับแก้ไข Model ที่มีอยู่ (PUT/PATCH)
|
||||
* Endpoint: PATCH /api/v1/models/{id}/ (Protected by IsAdminOrManager)
|
||||
*/
|
||||
export const useUpdateModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
// mutationFn รับ object ที่มี { id: number, data: object }
|
||||
mutationFn: async ({ id, data }) => {
|
||||
// เราใช้ PATCH เพื่อส่งเฉพาะฟิลด์ที่มีการเปลี่ยนแปลง
|
||||
const response = await axiosClient.patch(`/api/v1/models/${id}/`, data);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกแก้ไขสำเร็จแล้ว!');
|
||||
// Invalidate query list เพื่อบังคับให้ตารางอัปเดตข้อมูล
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การแก้ไขล้มเหลว: ${error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล'}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user