WIP: update: adaptations to use the new backend #1

Draft
arthur.wambst wants to merge 21 commits from dev into master
18 changed files with 1083 additions and 266 deletions

View File

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},

View File

@ -2,14 +2,18 @@ import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Navigation from "./component/Navigation/Navigation";
import Dashboard from "./pages/Dashboard";
import Practicals from "./pages/Practicals";
import Immersions from "./pages/Immersions";
import Instances from "./pages/Instances";
import Practical from "./pages/Practical";
import Immersion from "./pages/Immersion";
import Site from "./pages/Site";
import LoginPage from "./pages/Login";
import PageTest from "./pages/PageTest";
import CreateTp from "./pages/admin/CreateTp";
import CreateSite from "./pages/admin/CreateSite";
import Jis from "./pages/admin/Jis";
import BulkUsers from "./pages/admin/BulkCreateUser";
import Users from "./pages/admin/Users";
import Sites from "./pages/admin/Sites";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import AdminPage from "./pages/admin/AdminPage";
@ -21,15 +25,18 @@ function App() {
<Navigation>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/tps" element={<Practicals />} />
<Route path="/tps/:id" element={<Practical />} />
<Route path="/immersion" element={<Immersions />} />
<Route path="/immersion/:id" element={<Immersion />} />
<Route path="/site/:id" element={<Site />} />
<Route path="/instances" element={<Instances />} />
<Route path="/profile" element={<PageTest />} />
<Route path="/login" element={<LoginPage />} />
<Route path="admin" element={<AdminPage />} />
<Route path="/admin/jdmi" element={<BulkUsers />} />
<Route path="/admin/tps" element={<CreateTp />} />
<Route path="/admin/ji" element={<Jis />} />
<Route path="/admin/users" element={<Users />} />
<Route path="/admin/sites" element={<Sites />} />
<Route path="/settings" element={<CreateTp />} />
</Routes>
</Navigation>

View File

@ -41,7 +41,7 @@ const Navigation: React.FC<NavigationProps> = ({
};
useEffect(() => {
axios.get("/api/users/me").then((res) => {
axios.get("/api/user/me").then((res) => {
if (res.data.username.trim() === "") {
navigate("/login");
}
@ -138,15 +138,28 @@ const Navigation: React.FC<NavigationProps> = ({
<li>
<div>
<Link
to="/admin/tps"
to="/admin/ji"
onClick={toggleDrawer}
className="w-60"
>
Create Tp
JDMI
</Link>
</div>
</li>
<li>
<div>
<Link
to="/admin/sites"
onClick={toggleDrawer}
className="w-60"
>
Sites
</Link>
</div>
</li>
<li>
<div>
<Link
to="/admin/users"
@ -183,25 +196,11 @@ const Navigation: React.FC<NavigationProps> = ({
<li>
<div>
<DocumentTextIcon className="size-6" />
<Link to="/tps" onClick={toggleDrawer}>
TPs
<Link to="/immersion" onClick={toggleDrawer}>
Immersions
</Link>
</div>
</li>
<li>
<div>
<CommandLineIcon className="size-6" />
<Link to="/instances" onClick={toggleDrawer}>
Instances
</Link>
</div>
</li>
<li>
<div onClick={toggleDrawer}>
<EnvelopeIcon className="size-6" />
<Link to="/messages">Messages</Link>
</div>
</li>
</div>
</div>
)}

View File

@ -1,16 +1,25 @@
import { useEffect, useState } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import { Tp } from "../type/TpType";
import { Sujet } from "../type/SujetType";
import { Ji } from "../type/JiType";
import { DashboardType } from "../type/Dashboard";
function Dashboard() {
const [dashboard, setDashboard] = useState<DashboardType | null>(null);
const username = localStorage.getItem("username");
useEffect(() => {
axios.get("/api/dashboard").then((res) => {
setDashboard(res.data);
});
axios
.get("/api/dashboard")
.then((res) => {
setDashboard(res.data);
})
.catch((err) => {
console.log(err);
if (err.response.status === 401 || err.response.status === 403) {
window.location.href = "/login";
}
});
}, []);
return (
@ -27,22 +36,99 @@ function Dashboard() {
</section>
{dashboard && (
<>
{dashboard.jiRespo.length !== 0 && (
<section className="py-16">
<div className="container mx-auto">
<h3 className="text-2xl font-bold text-center mb-6">
Immersions - Respo
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-6">
{dashboard.jiRespo.map((jiRespo: Ji) => (
<div
key={jiRespo.id}
className="card card-compact bg-base-200 shadow-lg p-4"
>
<div className="card-body">
<h2 className="card-title">{jiRespo.name}</h2>
<p>{jiRespo.description}</p>
<div className="card-actions justify-end">
<Link
to={`/immersion/${jiRespo.id}`}
className="btn btn-primary btn-sm"
>
GO
</Link>
</div>
</div>
</div>
))}
</div>
</div>
</section>
)}
{dashboard.sujetRespo.length !== 0 && (
<section className="py-8">
<div className="container mx-auto">
<h3 className="text-2xl font-bold text-center mb-6">
Sujets - Respo
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{dashboard.sujetRespo.map((sujet: Sujet) => (
<div
key={sujetRespo.id}
className="card card-compact bg-base-200 shadow-lg p-4"
>
<div className="card-body">
<h2 className="card-title">{sujetRespo.name}</h2>
{sujetRespo && (
<p className="text-lg">
Linked to: {sujetRespo.name}
</p>
)}
<div className="card-actions justify-end">
{false && (
<Link
to={`/subject/${sujetRespo.id}`}
className="btn btn-primary"
>
Learn More
</Link>
)}
{sujetRespo && (
<Link
to={`/subject/${sujetRespo.id}`}
className="btn btn-primary"
>
See TP
</Link>
)}
</div>
</div>
</div>
))}
</div>
</div>
</section>
)}
<section className="py-16">
<div className="container mx-auto">
<h3 className="text-2xl font-bold text-center mb-6">
Practicals
Immersions - Activites
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{dashboard.tps.map((tp: Tp) => (
{dashboard.jiParticipant.map((jiParticipant: Ji) => (
<div
key={tp.id}
key={jiParticipant.id}
className="card card-compact bg-base-200 shadow-lg p-4"
>
<div className="card-body">
<h2 className="card-title">{tp.name}</h2>
<p>{tp.description}</p>
<h2 className="card-title">{jiParticipant.name}</h2>
<p>{jiParticipant.description}</p>
<div className="card-actions justify-end">
<Link to={`/tps/${tp.id}`} className="btn btn-primary">
<Link
to={`/immersion/${jiParticipant.id}`}
className="btn btn-primary"
>
Learn More
</Link>
</div>
@ -50,52 +136,15 @@ function Dashboard() {
</div>
))}
</div>
{dashboard.tps.length === 0 && (
<h1 className="text-xl">You have no tps</h1>
{dashboard.jiParticipant.length === 0 && (
<div className="container mx-auto text-center">
<h1 className="text-xl">
You are not registered on any activities.
</h1>
</div>
)}
</div>
</section>
<section className="py-8">
<div className="container mx-auto">
<h3 className="text-2xl font-bold text-center mb-6">Instances</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{dashboard.instances.map((instance) => (
<div
key={instance.id}
className="card card-compact bg-base-200 shadow-lg p-4"
>
<div className="card-body">
<h2 className="card-title">{instance.name}</h2>
{instance.tp && (
<p className="text-lg">Linked to: {instance.tp.name}</p>
)}
<div className="card-actions justify-end">
{false && (
<Link
to={`/instances/${instance.id}`}
className="btn btn-primary"
>
Learn More
</Link>
)}
{instance.tp && (
<Link
to={`/tps/${instance.tp.id}`}
className="btn btn-primary"
>
See TP
</Link>
)}
</div>
</div>
</div>
))}
{dashboard.instances.length === 0 && (
<h1 className="text-xl">You have no instances</h1>
)}
</div>
</div>
</section>
<section className="py-8">
<div className="container mx-auto text-center">
<h4 className="text-2xl font-semibold">Messages</h4>

393
src/pages/Immersion.tsx Normal file
View File

@ -0,0 +1,393 @@
import { ArrowDownTrayIcon, ClipboardIcon } from "@heroicons/react/24/outline";
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { Ji } from "../type/JiType";
import { Instance } from "../type/InstanceType";
function Immersion() {
const { id } = useParams();
const [ji, setJi] = useState<Ji>();
const [instance, setInstance] = useState<Instance>();
const [allInstances, setAllInstances] = useState<Instance[]>([]);
const [instancesStatus, setInstancesStatus] = useState<Record<number, string>>({});
const [containerStatus, setContainerStatus] = useState<string>("");
const [instancesOwner, setInstancesOwner] = useState<string>("");
const username = localStorage.getItem("username");
const copyText = (copy: string) => {
navigator.clipboard.writeText(copy);
toast.success("Copied!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
};
useEffect(() => {
axios.get(`/api/ji/${id}`).then((res) => {
setJi(res.data);
});
}, [id]);
useEffect(() => {
axios.get(`/api/ji/${id}/instances`).then((res) => {
setInstance(res.data);
});
}, [id]);
useEffect(() => {
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
// Récupérer l owner de chaque instance
res.data.forEach((inst: Instance) => {
axios.get(`/api/ji/${id}/instance-owner?instId=${inst.id}`).then((ownerRes) => {
setInstancesOwner(prev => ({
...prev,
[inst.id]: ownerRes.data
}));
});
});
});
}, [id]);
useEffect(() => {
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
// Récupérer le status de chaque instance
res.data.forEach((inst: Instance) => {
axios.get(`/api/ji/${id}/container-admin?instId=${inst.id}`).then((statusRes) => {
setInstancesStatus(prev => ({
...prev,
[inst.id]: statusRes.data
}));
});
});
});
}, [id]);
// Mise à jour automatique des status toutes les 5 secondes
useEffect(() => {
const intervalId = setInterval(() => {
if (allInstances.length > 0) {
allInstances.forEach((inst: Instance) => {
axios.get(`/api/ji/${id}/container-admin?instId=${inst.id}`).then((statusRes) => {
setInstancesStatus(prev => ({
...prev,
[inst.id]: statusRes.data
}));
});
});
}
}, 5000);
return () => clearInterval(intervalId);
}, [id, allInstances]);
useEffect(() => {
axios.get("/api/user/me").then((res) => {
localStorage.setItem("username", res.data.username);
});
}, []);
useEffect(() => {
axios.get(`/api/ji/${id}/container`).then((res) => {
setContainerStatus(res.data);
localStorage.setItem("container_status", res.data);
});
}, [id]);
// Mise à jour automatique du container status toutes les 5 secondes
useEffect(() => {
const intervalId = setInterval(() => {
axios.get(`/api/ji/${id}/container`).then((res) => {
setContainerStatus(res.data);
localStorage.setItem("container_status", res.data);
});
}, 5000);
return () => clearInterval(intervalId);
}, [id]);
const container_status = localStorage.getItem("container_status");
const handleCreateContainers = () => {
axios.post(`/api/ji/${id}/containers`).then((res) => {
if (res.status === 200) {
toast.success("Containers created successfully!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
// Recharger les instances pour mettre à jour les status
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
});
}
}).catch((error) => {
toast.error("Failed to create containers", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
});
};
const handleStartAllContainers = () => {
axios.post(`/api/ji/${id}/container/start`).then((res) => {
if (res.status === 200) {
toast.success("All containers started successfully!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
// Recharger les instances pour mettre à jour les status
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
});
}
}).catch((error) => {
toast.error("Failed to start containers", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
});
};
const handleStopAllContainers = () => {
axios.post(`/api/ji/${id}/container/stop`).then((res) => {
if (res.status === 200) {
toast.success("All containers stopped successfully!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
// Recharger les instances pour mettre à jour les status
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
});
}
}).catch((error) => {
toast.error("Failed to stop containers", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
});
};
const handleDeleteContainer = (instanceId: number) => {
axios.delete(`/api/ji/${id}/container/${instanceId}`).then((res) => {
if (res.status === 200) {
toast.success("Container deleted successfully!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
// Recharger les instances pour mettre à jour la liste
axios.get(`/api/ji/${id}/all-instances`).then((res) => {
setAllInstances(res.data);
});
}
}).catch((error) => {
toast.error("Failed to delete container", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
});
};
return (
<>
{ji && (
<>
<h1 className="text-3xl font-bold mb-2 ms-10">{ji.name}</h1>
<div className="px-6">
{/* Section supérieure : Info + iframe */}
<div className="flex flex-col lg:flex-row gap-6">
{/* Colonne gauche - Informations */}
<div className="lg:w-1/3 px-4">
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-4">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
{ji.name} - Information
</h2>
<ul className="space-y-4">
<li>{ji.description}</li>
<li className="flex justify-between">
<span>Description:</span>
<span className="font-medium">{ji.description}</span>
</li>
<li className="flex justify-between">
<span>Date:</span>
<span className="font-medium">{ji.date}</span>
</li>
</ul>
</div>
{instance && (
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-6">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
Your credentials :
</h2>
<ul className="space-y-2">
<li className="flex justify-between align-middle items-center">
<span>SSH:</span>
<div
className="flex gap-2 items-center cursor-pointer"
onClick={() => copyText(`ssh -p ${instance.port} ${username}@la-banquise.fr`)}
>
<span className="font-medium">ssh -p {instance.port} {username}@la-banquise.fr</span>
<div className="h-8 w-8 hover:bg-base-100 align-middle cursor-pointer flex items-center justify-center rounded">
<ClipboardIcon className="size-6" />
</div>
</div>
</li>
<li className="flex justify-between align-middle items-center">
<span>Instance name:</span>
<div className="flex gap-2 items-center">
<span className="font-medium">{instance.name}</span>
</div>
</li>
<li className="flex justify-between align-middle items-center">
<span>Password:</span>
<div
className="flex gap-2 items-center cursor-pointer"
onClick={() => copyText(instance.password)}
>
<span className="font-normal">{instance.password}</span>
<div className="h-8 w-8 hover:bg-base-100 align-middle cursor-pointer flex items-center justify-center rounded">
<ClipboardIcon className="size-6" />
</div>
</div>
</li>
<li className="flex justify-between align-middle items-center">
<span>Status:</span>
<div className="flex gap-2 items-center">
<span className="font-normal">{containerStatus}</span>
</div>
</li>
</ul>
</div>
)}
</div>
{/* Colonne droite - iframe du sujet */}
<div className="lg:w-2/3 px-4">
<div className="bg-base-200 shadow-lg rounded-lg p-6 mt-4 h-full">
<h2 className="text-2xl font-bold text-blue-600 mb-4">
Subject
</h2>
<iframe
src="https://tp.la-banquise.fr/Sujet_1_Banquise.pdf"
className="w-full h-[800px] rounded-lg border-2 border-base-300"
title="Subject Document"
/>
</div>
</div>
</div>
{/* Section inférieure : Tableau des instances sur toute la largeur */}
{username === "root" && (
<div className="px-4 mt-8">
<div className="bg-base-200 shadow-lg rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold text-blue-600">
All Instances
</h2>
<div className="flex gap-2">
<button
onClick={handleCreateContainers}
className="btn btn-success"
>
Create All Containers
</button>
<button
onClick={handleStartAllContainers}
className="btn btn-primary"
>
Start All
</button>
<button
onClick={handleStopAllContainers}
className="btn btn-error"
>
Stop All
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="table w-full">
<thead>
<tr>
<th className="p-4">#</th>
<th className="p-4">Name</th>
<th className="p-4">Port</th>
<th className="p-4">Username</th>
<th className="p-4">Password</th>
<th className="p-4">Status</th>
<th className="p-4">Actions</th>
</tr>
</thead>
<tbody>
{allInstances.length > 0 ? (
allInstances.map((inst: Instance, index: number) => (
<tr key={inst.id || index} className="hover:bg-base-300">
<td className="p-4">{index + 1}</td>
<td className="p-4">{inst.name}</td>
<td className="p-4">{inst.port}</td>
<td className="p-4">
<span className="badge badge-primary">
{instancesOwner[inst.id] || "Loading..."}
</span>
</td>
<td className="p-4">
<div className="flex items-center gap-2">
<span className="font-mono">{inst.password}</span>
<button
onClick={() => copyText(inst.password)}
className="btn btn-sm btn-ghost"
>
<ClipboardIcon className="size-4" />
</button>
</div>
</td>
<td className="p-4">
<span className="badge badge-primary">
{instancesStatus[inst.id] || "Loading..."}
</span>
</td>
<td className="p-4">
<div className="flex gap-2">
<button
className="btn btn-sm btn-primary"
onClick={() => copyText(`ssh -p ${inst.port} ${instancesOwner[inst.id] || "Loading..."}@la-banquise.fr`)}
>
Copy SSH
</button>
<button
className="btn btn-sm btn-error"
onClick={() => handleDeleteContainer(inst.id)}
>
Delete Container
</button>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="text-center p-8 text-gray-500">
No instances found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
</>
)}
</>
);
}
export default Immersion;

View File

@ -1,32 +1,32 @@
import { useEffect, useState } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import { Tp } from "../type/TpType";
import { Ji } from "../type/JiType";
function Practicals() {
const [tps, setTps] = useState([]);
function Immersions() {
const [jis, setJis] = useState([]);
useEffect(() => {
axios.get("/api/tps").then((res) => {
setTps(res.data);
axios.get("/api/ji/listall").then((res) => {
setJis(res.data);
});
}, []);
return (
<div className="container mx-auto py-10">
<h1 className="text-3xl font-bold mb-6 text-center">Your TPs</h1>
<h1 className="text-3xl font-bold mb-6 text-center">Journees d immersion - Activities</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tps.map((practical: Tp) => (
{jis.map((ji: Ji) => (
<div
key={practical.id}
key={ji.id}
className="card card-compact bg-base-200 shadow-lg p-4"
>
<div className="card-body">
<h2 className="card-title">{practical.name}</h2>
<p>{practical.description}</p>
<h2 className="card-title">{ji.name}</h2>
<p>{ji.description}</p>
<div className="card-actions justify-end">
<Link to={`/tps/${practical.id}`} className="btn btn-primary">
Learn More
<Link to={`/immersion/${ji.id}`} className="btn btn-primary">
Open activity
</Link>
</div>
</div>
@ -37,4 +37,4 @@ function Practicals() {
);
}
export default Practicals;
export default Immersions;

View File

@ -22,9 +22,9 @@ const LoginPage: React.FC = () => {
})
.then((response) => {
if (response.status === 200) {
axios.get("/api/users/me").then((res) => {
axios.get("/api/user/me").then((res) => {
localStorage.setItem("username", res.data.username);
if (res.data.roles.includes("root")) {
if (res.data.roles.includes("ROOT")) {
localStorage.setItem("root", "true");
} else {
localStorage.removeItem("root");

View File

@ -1,135 +0,0 @@
import { ArrowDownTrayIcon, ClipboardIcon } from "@heroicons/react/24/outline";
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { Tp } from "../type/TpType";
function Practical() {
const { id } = useParams();
const [tp, setTp] = useState<Tp>();
const copyText = (copy: string) => {
navigator.clipboard.writeText(copy);
toast.success("Copied!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
};
useEffect(() => {
axios.get(`/api/tps/${id}`).then((res) => {
setTp(res.data);
});
}, [id]);
return (
<>
{tp && (
<>
<h1 className="text-3xl font-bold mb-2 ms-10">{tp.name}</h1>
<div className="px-6">
<div className="flex flex-col lg:flex-row">
<div className="lg:w-2/3 p-4">
<div className="w-full h-[80vh] bg-gray-200 rounded-lg overflow-hidden mb-4">
<iframe
src={tp.pdfLink}
className="w-full h-full"
title={`${tp.name} PDF`}
></iframe>
</div>
</div>
<div className="lg:w-1/3 px-4">
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-4">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
Information
</h2>
<ul className="space-y-4">
<li>{tp.description}</li>
<li className="flex justify-between">
<span>Duration:</span>
<span className="font-medium">2 hours</span>
</li>
<li className="flex justify-between">
<span>Tools Used:</span>
<span className="font-medium">Python, SSH, Vim</span>
</li>
<li className="flex justify-between">
<span>Difficulty:</span>
<span className="font-medium">Beginner</span>
</li>
<li className="flex justify-between">
<span>Date:</span>
<span className="font-medium">Oct 23, 2024</span>
</li>
</ul>
</div>
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-6">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
Resources
</h2>
<ul className="space-y-2">
<li className="flex justify-between align-middle items-center">
<span>SSH:</span>
<div
className="flex gap-2 items-center"
onClick={() => copyText(tp.ssh)}
>
<span className="font-medium">{tp.ssh}</span>
<div className="h-8 w-8 hover:bg-base-100 align-middle cursor-pointer flex items-center justify-center rounded">
<ClipboardIcon className="size-6" />
</div>
</div>
</li>
<li className="flex justify-between align-middle items-center">
<span>Port:</span>
<div
className="flex gap-2 items-center"
onClick={() => copyText(tp.port)}
>
<span className="font-medium">{tp.port}</span>
<div className="h-8 w-8 hover:bg-base-100 align-middle cursor-pointer flex items-center justify-center rounded">
<ClipboardIcon className="size-6" />
</div>
</div>
</li>
<li className="flex justify-between align-middle items-center">
<span>Password:</span>
<div
className="flex gap-2 items-center"
onClick={() => copyText(tp.pwd)}
>
<span className="font-normal">{tp.pwd}</span>
<div className="h-8 w-8 hover:bg-base-100 align-middle cursor-pointer flex items-center justify-center rounded">
<ClipboardIcon className="size-6" />
</div>
</div>
</li>
</ul>
</div>
<a
href={tp.pdfLink}
target="_blank"
className="card w-full bg-base-200 shadow-md hover:shadow-lg transition-shadow rounded-lg mt-6"
>
<div className="card-body flex-row justify-between items-center">
<div>
<div className="card-title text-lg font-bold">
Subject
</div>
<p className="text-base-content">Download</p>
</div>
<ArrowDownTrayIcon className="size-6" />
</div>
</a>
</div>
</div>
</div>
</>
)}
</>
);
}
export default Practical;

88
src/pages/Site.tsx Normal file
View File

@ -0,0 +1,88 @@
import { ArrowDownTrayIcon, ClipboardIcon } from "@heroicons/react/24/outline";
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { Site } from "../type/SiteType";
function Site() {
const { id } = useParams();
const [site, setSite] = useState<Site>();
const copyText = (copy: string) => {
navigator.clipboard.writeText(copy);
toast.success("Copied!", {
draggable: true,
theme: localStorage.getItem("theme") || "dark",
});
};
useEffect(() => {
axios.get(`/api/sites/${id}`).then((res) => {
setSite(res.data);
});
}, [id]);
return (
<>
{site && (
<>
<h1 className="text-3xl font-bold mb-2 ms-10">{site.name}</h1>
<div className="px-6">
<div className="flex flex-col lg:flex-row">
<div className="lg:w-2/3 p-4">
<div className="w-full h-[80vh] bg-gray-200 rounded-lg overflow-hidden mb-4">
{site.listJi}
</div>
</div>
<div className="lg:w-1/3 px-4">
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-4">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
{site.name} - Informations
</h2>
<ul className="space-y-4">
<li className="flex justify-between">
<span>Description:</span>
<span className="font-medium">{site.description}</span>
</li>
<li className="flex justify-between">
<span>Address:</span>
<span className="font-medium">{site.address}</span>
</li>
</ul>
</div>
<div className="bg-base-200 p-6 rounded-lg shadow-md mt-6">
<h2 className="text-2xl font-semibold text-blue-600 mb-2">
Responsables du site
</h2>
mettre une liste des respo site
<ul className="space-y-2">
</ul>
</div>
<a
href={site.name}
target="_blank"
className="card w-full bg-base-200 shadow-md hover:shadow-lg transition-shadow rounded-lg mt-6"
>
<div className="card-body flex-row justify-between items-center">
<div>
<div className="card-title text-lg font-bold">
Subject
</div>
<p className="text-base-content">Download</p>
</div>
<ArrowDownTrayIcon className="size-6" />
</div>
</a>
</div>
</div>
</div>
</>
)}
</>
);
}
export default Site;

View File

@ -2,22 +2,22 @@ import axios from "axios";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { BulkUserCreattionType } from "../../type/BulkUserCreattionType";
import { Tp } from "../../type/TpType";
import { Ji } from "../../type/TpType";
import { CSVParseType } from "../../type/CSVParseType";
function BulkUsers() {
const [userData, setUserData] = useState<CSVParseType[]>([]);
const [practical, setPractical] = useState("0");
const navigate = useNavigate();
const [tps, setTps] = useState([]);
const [jis, setJis] = useState([]);
const handlePracticalChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPractical(e.target.value);
};
useEffect(() => {
axios.get("/api/tps").then((res) => {
setTps(res.data);
axios.get("/api/ji/listall").then((res) => {
setJis(res.data);
});
}, []);
@ -49,14 +49,57 @@ function BulkUsers() {
reader.readAsText(file);
};
const handleSubmit = () => {
axios
.post("/api/users/jdmi", {
const handleSubmit = async () => {
try {
const response = await axios.post("/api/users", {
users: userData.filter((user: CSVParseType) => user.name !== ""),
tpId: practical,
})
.then(() => navigate(`/admin/users`));
jiId: parseInt(practical),
});
if (response.status === 200) {
// REMPLACER l'alert par ce code :
let str = "";
for (let i = 0; i < response.data.successMails.length; i++) {
const mail = response.data.successMails[i];
const password = response.data.successPasswd[i];
str += `${mail},${password}\n`;
}
const url = window.URL.createObjectURL(
new Blob([str], { type: "text/csv" }),
);
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
`created_users_${new Date().toISOString().split("T")[0]}.csv`,
);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
alert("Users succesfully created ! CSV downloaded.");
navigate("/admin/users");
}
if (response.status === 202)
{ const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `created_users_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
alert(`Couldn't create some users`);}
if (response.status === 500)
alert(`Couldn't create ANY users`);
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de la création des users');
};
};
return (
<div className="max-w-7xl mx-auto p-5">
@ -86,11 +129,6 @@ function BulkUsers() {
<tr key={index}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.password}</td>
<td>{user.instance_name}</td>
<td>{user.instance_ssh}</td>
<td>{user.instance_port}</td>
<td>{user.instance_pwd}</td>
</tr>
))}
</tbody>
@ -99,7 +137,7 @@ function BulkUsers() {
)}
<div>
<label className="block text-gray-700 font-semibold mb-2">Tp:</label>
<label className="block text-gray-700 font-semibold mb-2">Ji:</label>
<select
name="practical"
value={practical}
@ -107,15 +145,15 @@ function BulkUsers() {
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
return <option value="0">Empty</option>;
{tps.map((tp: Tp) => {
return <option value={tp.id}>{tp.name}</option>;
{jis.map((ji: Ji) => {
return <option value={ji.id}>{ji.name}</option>;
})}
</select>
</div>
<button
onClick={handleSubmit}
className="btn btn-primary mt-4"
disabled={userData.length === 0}
disabled={userData.length === 0 || practical == "0"}
>
Create Users
</button>

200
src/pages/admin/Jis.tsx Normal file
View File

@ -0,0 +1,200 @@
import axios from "axios";
import { useEffect, useState } from "react";
import { Ji } from "../../type/JiType";
function Jis() {
const [jis, setJis] = useState([]);
const [reload, setReload] = useState(0);
const [showCreateForm, setShowCreateForm] = useState(false);
const [newJi, setNewJi] = useState({
name: "",
desc: "",
respo: "",
site_id: "",
date: "",
});
useEffect(() => {
axios.get("/api/ji/listall").then((res) => {
setJis(res.data);
});
}, [reload]);
const handleEditJi = (jiId: number) => {
alert(`Edit ji with ID: ${jiId}`);
};
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setNewJi({ ...newJi, [name]: value });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const params = new URLSearchParams({
name: newJi.name,
respo: newJi.respo,
site_id: newJi.site_id,
date: newJi.date,
});
axios.post(`/api/ji/create?${params.toString()}`).then((res) => {
if (res.status === 200) {
setNewJi({ name: "", desc: "", respo: "", site_id: "", date: "" });
setShowCreateForm(false);
setReload(reload + 1);
}
});
};
return (
<div className="container mx-auto p-6">
<div className="bg-white shadow-lg rounded-lg p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-blue-600">
Manage JIs
</h1>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{showCreateForm ? "Cancel" : "Create New JI"}
</button>
</div>
{/* Create JI Form */}
{showCreateForm && (
<div className="mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Create New JI
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-gray-700 font-semibold mb-2">
Name:
</label>
<input
type="text"
name="name"
value={newJi.name}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter JI name"
required
/>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Description:
</label>
<textarea
name="desc"
value={newJi.desc}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter JI description"
rows={4}
required
></textarea>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Responsable:
</label>
<input
type="text"
name="respo"
value={newJi.respo}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter responsable name"
required
/>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Site ID:
</label>
<input
type="text"
name="site_id"
value={newJi.site_id}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter site ID"
required
/>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Date:
</label>
<input
type="date"
name="date"
value={newJi.date}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div className="text-right">
<button
type="submit"
className="bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500"
>
Create JI
</button>
</div>
</form>
</div>
)}
{/* JIs Table */}
<div className="overflow-x-auto">
<table className="table w-full">
<thead>
<tr>
<th className="p-4">#</th>
<th className="p-4">Name</th>
<th className="p-4">Responsable</th>
<th className="p-4">Site ID</th>
<th className="p-4">Date</th>
<th className="p-4">Actions</th>
</tr>
</thead>
<tbody>
{jis.map((ji: Ji, index: number) => (
<tr key={ji.id} className="hover:bg-gray-100">
<td className="p-4">{index + 1}</td>
<td className="p-4">{ji.name}</td>
<td className="p-4">{ji.respo}</td>
<td className="p-4">{ji.site_id}</td>
<td className="p-4">{ji.date}</td>
<td className="p-4">
<button
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-400"
onClick={() => handleEditJi(ji.id)}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
export default Jis;

163
src/pages/admin/Sites.tsx Normal file
View File

@ -0,0 +1,163 @@
import axios from "axios";
import { useEffect, useState } from "react";
function Sites() {
const [sites, setSites] = useState([]);
const [reload, setReload] = useState(0);
const [showCreateForm, setShowCreateForm] = useState(false);
const [site, setSite] = useState({
name: "",
desc: "",
address: "",
});
useEffect(() => {
axios.get("/api/sites").then((res) => {
setSites(res.data);
});
}, [reload]);
const handleEditSite = (siteId) => {
alert(`Edit site with ID: ${siteId}`);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setSite({ ...site, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const params = new URLSearchParams({
name: site.name,
description: site.desc,
address: site.address,
});
axios.post(`/api/sites?${params.toString()}`).then((res) => {
if (res.status === 200) {
setSite({ name: "", desc: "", address: "" });
setShowCreateForm(false);
setReload(reload + 1);
}
});
};
return (
<div className="container mx-auto p-6">
<div className="bg-white shadow-lg rounded-lg p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-blue-600">
Manage Sites
</h1>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{showCreateForm ? "Cancel" : "Create New Site"}
</button>
</div>
{/* Create Site Form */}
{showCreateForm && (
<div className="mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Create New Site
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-gray-700 font-semibold mb-2">
Name:
</label>
<input
type="text"
name="name"
value={site.name}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter site name"
required
/>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Description:
</label>
<textarea
name="desc"
value={site.desc}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter site description"
rows={4}
required
></textarea>
</div>
<div>
<label className="block text-gray-700 font-semibold mb-2">
Address:
</label>
<input
type="text"
name="address"
value={site.address}
onChange={handleInputChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter site's address"
required
/>
</div>
<div className="text-right">
<button
type="submit"
className="bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500"
>
Create Site
</button>
</div>
</form>
</div>
)}
{/* Sites Table */}
<div className="overflow-x-auto">
<table className="table w-full">
<thead>
<tr>
<th className="p-4">#</th>
<th className="p-4">Name</th>
<th className="p-4">Address</th>
<th className="p-4">Roles</th>
<th className="p-4">Actions</th>
</tr>
</thead>
<tbody>
{sites.map((site, index) => (
<tr key={site.id} className="hover:bg-gray-100">
<td className="p-4">{index + 1}</td>
<td className="p-4">{site.name}</td>
<td className="p-4">{site.address}</td>
<td className="p-4">{site.listJi}</td>
<td className="p-4">
<button
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-400"
onClick={() => handleEditSite(site.id)}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
export default Sites;

View File

@ -1,7 +1,8 @@
import { Instance } from "./InstanceType";
import { Tp } from "./TpType";
import { Ji } from "./JiType";
import { Sujet } from "./SujetType";
export interface DashboardType {
tps: Tp[];
instances: Instance[];
jiRespo: Ji[];
jiParticipant: Ji[];
sujetRespo: Sujet[];
}

View File

@ -1,11 +1,7 @@
import { Tp } from "./TpType";
import { User } from "./UserType";
export interface Instance {
id: number;
name: string;
ssh: string;
pwd: string;
user: User;
tp: Tp;
port: string;
}

13
src/type/JiType.ts Normal file
View File

@ -0,0 +1,13 @@
import { Sujet } from "./SujetType";
import { User } from "./UserType";
export interface Ji {
id: number,
name: string;
description: string;
date: string;
site: Site;
respos: User[];
participants: User[]
//instances: Instances[];
}

9
src/type/SiteType.ts Normal file
View File

@ -0,0 +1,9 @@
import { Ji } from "./JiType";
export interface Site {
id: number;
name: string;
description: string;
address: string;
listJi: List<Ji>;
}

7
src/type/SujetType.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Sujet {
id: number;
name: string;
description: string;
pdfLink: string;
respos: string;
}

View File

@ -1,11 +0,0 @@
export interface Tp {
id: number;
name: string;
description: string;
pdfLink: string;
respo: string;
date: string;
ssh: string;
port: string;
pwd: string;
}