เพิ่ม Toast Component
This commit is contained in:
parent
693ca097f4
commit
5670ef88e1
14
web/package-lock.json
generated
14
web/package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"uuid": "^13.0.0",
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -3834,6 +3835,19 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"uuid": "^13.0.0",
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -2,12 +2,13 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
|
||||
import AuthRoutes from "./routes/AuthRoutes.jsx";
|
||||
import PrivateRoutes from "./routes/PrivateRoutes.jsx";
|
||||
|
||||
import ToastNotification from './components/ToastNotification.jsx';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
// ต้องห่อหุ้มด้วย Provider ของ Redux และ QueryClient ใน main.jsx
|
||||
<BrowserRouter>
|
||||
<ToastNotification />
|
||||
<Routes>
|
||||
{/* Private Routes: ตรวจสอบล็อกอินก่อนเข้า /dashboard/* */}
|
||||
<Route path="/dashboard/*" element={<PrivateRoutes/>}/>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from '../features/auth/authSlice';
|
||||
import toastReducer from '../features/toast/toastSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
// กำหนด Reducer หลัก
|
||||
auth: authReducer,
|
||||
toast: toastReducer,
|
||||
// [เพิ่ม Reducer อื่น ๆ ที่นี่]
|
||||
},
|
||||
// ปิด serializableCheck ใน Production เพื่อประสิทธิภาพ
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTestConnection } from '../../services/modelRegistryApi'; // Hook สำหรับ Test Connection
|
||||
import { FaCheckCircle, FaExclamationTriangle, FaTrash, FaEdit } from 'react-icons/fa';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addToast } from '../../features/toast/toastSlice';
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Helper Function: แสดง Badge สถานะ
|
||||
@ -17,7 +18,10 @@ const getStatusBadge = (status) => {
|
||||
};
|
||||
|
||||
function ModelTable({ models, handleOpenEdit, handleDelete, deleteLoading }) {
|
||||
// 1. Hook สำหรับทดสอบการเชื่อมต่อ (Test Connection)
|
||||
// ประกาศ useDispatch Hook
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Hook สำหรับทดสอบการเชื่อมต่อ (Test Connection)
|
||||
// ใช้ Hook ตรงนี้ เพราะเป็น Logic ที่เกี่ยวข้องกับ Action ในตารางโดยตรง
|
||||
const testConnectionMutation = useTestConnection();
|
||||
|
||||
@ -26,10 +30,18 @@ function ModelTable({ models, handleOpenEdit, handleDelete, deleteLoading }) {
|
||||
testConnectionMutation.mutate(modelId, {
|
||||
onSuccess: (result) => {
|
||||
const status = result.status === 'success' ? 'SUCCESS' : 'FAILED';
|
||||
alert(`Test Result: ${status}\nMessage: ${result.message || JSON.stringify(result.response_data)}`);
|
||||
const message = `Test Result: ${status}. Service responded successfully.`;
|
||||
dispatch(addToast({
|
||||
message: message,
|
||||
type: status === 'SUCCESS' ? 'success' : 'error'
|
||||
}));
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`Test Failed! Error: ${error.message || 'Check network or base URL'}`);
|
||||
const errorMessage = error.message || 'Check network or base URL';
|
||||
dispatch(addToast({
|
||||
message: `Test Failed: ${errorMessage}`,
|
||||
type: 'error'
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
137
web/src/components/ToastNotification.jsx
Normal file
137
web/src/components/ToastNotification.jsx
Normal file
@ -0,0 +1,137 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { removeToast } from '../features/toast/toastSlice';
|
||||
|
||||
const TOAST_DURATION = 3000;
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Helper Functions
|
||||
// ----------------------------------------------------
|
||||
|
||||
const getColors = (type) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'alert-success';
|
||||
case 'error':
|
||||
return 'alert-error';
|
||||
case 'warning':
|
||||
return 'alert-warning';
|
||||
case 'info':
|
||||
return 'alert-info';
|
||||
default:
|
||||
return 'alert-info';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667
|
||||
1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464
|
||||
0L3.332 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'info':
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21
|
||||
12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
// ToastNotification Component
|
||||
// ----------------------------------------------------
|
||||
|
||||
export default function ToastNotification() {
|
||||
const dispatch = useDispatch();
|
||||
const queue = useSelector((state) => state.toast.queue);
|
||||
|
||||
useEffect(() => {
|
||||
if (queue.length > 0) {
|
||||
queue.forEach((toast) => {
|
||||
if (!toast.timerId) {
|
||||
setTimeout(() => {
|
||||
dispatch(removeToast({ id: toast.id }));
|
||||
}, TOAST_DURATION);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [queue, dispatch]);
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 right-0 z-[100] p-4 space-y-3 pointer-events-none">
|
||||
{queue.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`alert ${getColors(toast.type)} shadow-xl transition-all duration-500 ease-out transform translate-x-0 opacity-100 max-w-sm`}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
onClick={() => dispatch(removeToast({ id: toast.id }))}
|
||||
>
|
||||
<div className="flex items-center space-x-2 text-white">
|
||||
{getIcon(toast.type)}
|
||||
<span>{toast.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
web/src/features/toast/toastSlice.js
Normal file
31
web/src/features/toast/toastSlice.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TOAST_DURATION = 3000; // 3 วินาที
|
||||
|
||||
const toastSlice = createSlice({
|
||||
name: 'toast',
|
||||
initialState: {
|
||||
queue: [], // สถานะเก็บรายการ Toast ทั้งหมดที่ต้องแสดง
|
||||
},
|
||||
reducers: {
|
||||
// Action สำหรับเพิ่ม Toast เข้าคิว
|
||||
addToast: (state, action) => {
|
||||
const newToast = {
|
||||
id: uuidv4(),
|
||||
message: action.payload.message,
|
||||
type: action.payload.type || 'info',
|
||||
};
|
||||
state.queue.push(newToast);
|
||||
|
||||
},
|
||||
|
||||
// Action สำหรับลบ Toast ออกจากคิว
|
||||
removeToast: (state, action) => {
|
||||
state.queue = state.queue.filter(toast => toast.id !== action.payload.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const { addToast, removeToast } = toastSlice.actions;
|
||||
export default toastSlice.reducer;
|
||||
@ -1,5 +1,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import axiosClient from './axiosClient'; // Axios Client ที่มี JWT Interceptor
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addToast } from '../features/toast/toastSlice';
|
||||
|
||||
const STALE_TIME = 60 * 1000; // 1 นาที
|
||||
|
||||
@ -32,6 +34,7 @@ export const useModelList = () => {
|
||||
*/
|
||||
export const useCreateModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dispatch = useDispatch();
|
||||
return useMutation({
|
||||
mutationFn: async (modelData) => {
|
||||
// modelData: { name, model_version, base_url, inference_path, ... }
|
||||
@ -39,11 +42,12 @@ export const useCreateModel = () => {
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกลงทะเบียนสำเร็จแล้ว!');
|
||||
dispatch(addToast({ message: 'Model ถูกลงทะเบียนสำเร็จแล้ว!', type: 'success' }));
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การลงทะเบียนล้มเหลว: ${error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล'}`);
|
||||
const errorMessage = error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล';
|
||||
dispatch(addToast({ message: `การลงทะเบียนล้มเหลว: ${errorMessage}`, type: 'error' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -54,17 +58,19 @@ export const useCreateModel = () => {
|
||||
*/
|
||||
export const useDeleteModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dispatch = useDispatch();
|
||||
return useMutation({
|
||||
mutationFn: async (modelId) => {
|
||||
const response = await axiosClient.delete(`/api/v1/models/${modelId}/`);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกลบสำเร็จแล้ว!');
|
||||
dispatch(addToast({ message: 'Model ถูกลบสำเร็จแล้ว!', type: 'success' }));
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การลบล้มเหลว: ${error.response?.data?.detail || 'คุณอาจไม่มีสิทธิ์'}`);
|
||||
const errorMessage = error.response?.data?.detail || 'คุณอาจไม่มีสิทธิ์';
|
||||
dispatch(addToast({ message: `การลบล้มเหลว: ${errorMessage}`, type: 'error' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -89,6 +95,7 @@ export const useTestConnection = () => {
|
||||
*/
|
||||
export const useUpdateModel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dispatch = useDispatch();
|
||||
return useMutation({
|
||||
// mutationFn รับ object ที่มี { id: number, data: object }
|
||||
mutationFn: async ({ id, data }) => {
|
||||
@ -97,12 +104,13 @@ export const useUpdateModel = () => {
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert('Model ถูกแก้ไขสำเร็จแล้ว!');
|
||||
dispatch(addToast({ message: 'Model ถูกแก้ไขสำเร็จแล้ว!', type: 'success' }));
|
||||
// Invalidate query list เพื่อบังคับให้ตารางอัปเดตข้อมูล
|
||||
queryClient.invalidateQueries({ queryKey: ['modelList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`การแก้ไขล้มเหลว: ${error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล'}`);
|
||||
const errorMessage = error.response?.data?.detail || 'โปรดตรวจสอบข้อมูล';
|
||||
dispatch(addToast({ message: `การแก้ไขล้มเหลว: ${errorMessage}`, type: 'error' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user