Compare commits
No commits in common. "dev" and "master" have entirely different histories.
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
||||
15
src/App.tsx
15
src/App.tsx
@ -2,18 +2,14 @@ import "./App.css";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import Navigation from "./component/Navigation/Navigation";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Immersions from "./pages/Immersions";
|
||||
import Practicals from "./pages/Practicals";
|
||||
import Instances from "./pages/Instances";
|
||||
import Immersion from "./pages/Immersion";
|
||||
import Site from "./pages/Site";
|
||||
import Practical from "./pages/Practical";
|
||||
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";
|
||||
@ -25,18 +21,15 @@ function App() {
|
||||
<Navigation>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/immersion" element={<Immersions />} />
|
||||
<Route path="/immersion/:id" element={<Immersion />} />
|
||||
<Route path="/site/:id" element={<Site />} />
|
||||
<Route path="/tps" element={<Practicals />} />
|
||||
<Route path="/tps/:id" element={<Practical />} />
|
||||
<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>
|
||||
|
||||
@ -41,7 +41,7 @@ const Navigation: React.FC<NavigationProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/user/me").then((res) => {
|
||||
axios.get("/api/users/me").then((res) => {
|
||||
if (res.data.username.trim() === "") {
|
||||
navigate("/login");
|
||||
}
|
||||
@ -138,28 +138,15 @@ const Navigation: React.FC<NavigationProps> = ({
|
||||
<li>
|
||||
<div>
|
||||
<Link
|
||||
to="/admin/ji"
|
||||
to="/admin/tps"
|
||||
onClick={toggleDrawer}
|
||||
className="w-60"
|
||||
>
|
||||
JDMI
|
||||
Create Tp
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div>
|
||||
<Link
|
||||
to="/admin/sites"
|
||||
onClick={toggleDrawer}
|
||||
className="w-60"
|
||||
>
|
||||
Sites
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div>
|
||||
<Link
|
||||
to="/admin/users"
|
||||
@ -196,11 +183,25 @@ const Navigation: React.FC<NavigationProps> = ({
|
||||
<li>
|
||||
<div>
|
||||
<DocumentTextIcon className="size-6" />
|
||||
<Link to="/immersion" onClick={toggleDrawer}>
|
||||
Immersions
|
||||
<Link to="/tps" onClick={toggleDrawer}>
|
||||
TPs
|
||||
</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>
|
||||
)}
|
||||
|
||||
@ -1,25 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Sujet } from "../type/SujetType";
|
||||
import { Ji } from "../type/JiType";
|
||||
import { Tp } from "../type/TpType";
|
||||
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);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if (err.response.status === 401 || err.response.status === 403) {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
});
|
||||
axios.get("/api/dashboard").then((res) => {
|
||||
setDashboard(res.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -36,99 +27,22 @@ 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">
|
||||
Immersions - Activites
|
||||
Practicals
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{dashboard.jiParticipant.map((jiParticipant: Ji) => (
|
||||
{dashboard.tps.map((tp: Tp) => (
|
||||
<div
|
||||
key={jiParticipant.id}
|
||||
key={tp.id}
|
||||
className="card card-compact bg-base-200 shadow-lg p-4"
|
||||
>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{jiParticipant.name}</h2>
|
||||
<p>{jiParticipant.description}</p>
|
||||
<h2 className="card-title">{tp.name}</h2>
|
||||
<p>{tp.description}</p>
|
||||
<div className="card-actions justify-end">
|
||||
<Link
|
||||
to={`/immersion/${jiParticipant.id}`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<Link to={`/tps/${tp.id}`} className="btn btn-primary">
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
@ -136,15 +50,52 @@ function Dashboard() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{dashboard.jiParticipant.length === 0 && (
|
||||
<div className="container mx-auto text-center">
|
||||
<h1 className="text-xl">
|
||||
You are not registered on any activities.
|
||||
</h1>
|
||||
</div>
|
||||
{dashboard.tps.length === 0 && (
|
||||
<h1 className="text-xl">You have no tps</h1>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@ -1,393 +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 { 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;
|
||||
@ -22,9 +22,9 @@ const LoginPage: React.FC = () => {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
axios.get("/api/user/me").then((res) => {
|
||||
axios.get("/api/users/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");
|
||||
|
||||
135
src/pages/Practical.tsx
Normal file
135
src/pages/Practical.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
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;
|
||||
@ -1,32 +1,32 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Ji } from "../type/JiType";
|
||||
import { Tp } from "../type/TpType";
|
||||
|
||||
function Immersions() {
|
||||
const [jis, setJis] = useState([]);
|
||||
function Practicals() {
|
||||
const [tps, setTps] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/ji/listall").then((res) => {
|
||||
setJis(res.data);
|
||||
axios.get("/api/tps").then((res) => {
|
||||
setTps(res.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<h1 className="text-3xl font-bold mb-6 text-center">Journees d immersion - Activities</h1>
|
||||
<h1 className="text-3xl font-bold mb-6 text-center">Your TPs</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{jis.map((ji: Ji) => (
|
||||
{tps.map((practical: Tp) => (
|
||||
<div
|
||||
key={ji.id}
|
||||
key={practical.id}
|
||||
className="card card-compact bg-base-200 shadow-lg p-4"
|
||||
>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{ji.name}</h2>
|
||||
<p>{ji.description}</p>
|
||||
<h2 className="card-title">{practical.name}</h2>
|
||||
<p>{practical.description}</p>
|
||||
<div className="card-actions justify-end">
|
||||
<Link to={`/immersion/${ji.id}`} className="btn btn-primary">
|
||||
Open activity
|
||||
<Link to={`/tps/${practical.id}`} className="btn btn-primary">
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,4 +37,4 @@ function Immersions() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Immersions;
|
||||
export default Practicals;
|
||||
@ -1,88 +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 { 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;
|
||||
@ -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 { Ji } from "../../type/TpType";
|
||||
import { Tp } from "../../type/TpType";
|
||||
import { CSVParseType } from "../../type/CSVParseType";
|
||||
|
||||
function BulkUsers() {
|
||||
const [userData, setUserData] = useState<CSVParseType[]>([]);
|
||||
const [practical, setPractical] = useState("0");
|
||||
const navigate = useNavigate();
|
||||
const [jis, setJis] = useState([]);
|
||||
const [tps, setTps] = useState([]);
|
||||
|
||||
const handlePracticalChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPractical(e.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/ji/listall").then((res) => {
|
||||
setJis(res.data);
|
||||
axios.get("/api/tps").then((res) => {
|
||||
setTps(res.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -49,57 +49,14 @@ function BulkUsers() {
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const response = await axios.post("/api/users", {
|
||||
const handleSubmit = () => {
|
||||
axios
|
||||
.post("/api/users/jdmi", {
|
||||
users: userData.filter((user: CSVParseType) => user.name !== ""),
|
||||
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');
|
||||
tpId: practical,
|
||||
})
|
||||
.then(() => navigate(`/admin/users`));
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto p-5">
|
||||
@ -129,6 +86,11 @@ 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>
|
||||
@ -137,7 +99,7 @@ function BulkUsers() {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 font-semibold mb-2">Ji:</label>
|
||||
<label className="block text-gray-700 font-semibold mb-2">Tp:</label>
|
||||
<select
|
||||
name="practical"
|
||||
value={practical}
|
||||
@ -145,15 +107,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>;
|
||||
{jis.map((ji: Ji) => {
|
||||
return <option value={ji.id}>{ji.name}</option>;
|
||||
{tps.map((tp: Tp) => {
|
||||
return <option value={tp.id}>{tp.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary mt-4"
|
||||
disabled={userData.length === 0 || practical == "0"}
|
||||
disabled={userData.length === 0}
|
||||
>
|
||||
Create Users
|
||||
</button>
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
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;
|
||||
@ -1,163 +0,0 @@
|
||||
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;
|
||||
@ -1,8 +1,7 @@
|
||||
import { Ji } from "./JiType";
|
||||
import { Sujet } from "./SujetType";
|
||||
import { Instance } from "./InstanceType";
|
||||
import { Tp } from "./TpType";
|
||||
|
||||
export interface DashboardType {
|
||||
jiRespo: Ji[];
|
||||
jiParticipant: Ji[];
|
||||
sujetRespo: Sujet[];
|
||||
tps: Tp[];
|
||||
instances: Instance[];
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Tp } from "./TpType";
|
||||
import { User } from "./UserType";
|
||||
|
||||
export interface Instance {
|
||||
id: number;
|
||||
name: string;
|
||||
port: string;
|
||||
ssh: string;
|
||||
pwd: string;
|
||||
user: User;
|
||||
tp: Tp;
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
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[];
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { Ji } from "./JiType";
|
||||
|
||||
export interface Site {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
address: string;
|
||||
listJi: List<Ji>;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export interface Sujet {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
pdfLink: string;
|
||||
respos: string;
|
||||
}
|
||||
11
src/type/TpType.ts
Normal file
11
src/type/TpType.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface Tp {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
pdfLink: string;
|
||||
respo: string;
|
||||
date: string;
|
||||
ssh: string;
|
||||
port: string;
|
||||
pwd: string;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user