Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7384837c3 | ||
|
|
6ef6545814 | ||
|
|
8af8d2aef5 | ||
|
|
5855ea4100 | ||
|
|
77ada7394e | ||
|
|
fb0b5053d7 | ||
|
|
8ebd3aca04 | ||
|
|
e0b950f1c4 | ||
|
|
014a476177 | ||
|
|
a24db1f53f | ||
|
|
ddcb0920c5 | ||
|
|
3557050f8e | ||
|
|
0a7e3fcc44 | ||
|
|
72c660f688 | ||
|
|
149ec43b02 | ||
|
|
afc23d1208 | ||
|
|
a75b432aa4 | ||
|
|
69955ac433 | ||
|
|
8c04856614 | ||
|
|
31ffd227c7 | ||
|
|
42dea97763 |
@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@ -2,14 +2,18 @@ import "./App.css";
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import Navigation from "./component/Navigation/Navigation";
|
import Navigation from "./component/Navigation/Navigation";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import Practicals from "./pages/Practicals";
|
import Immersions from "./pages/Immersions";
|
||||||
import Instances from "./pages/Instances";
|
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 LoginPage from "./pages/Login";
|
||||||
import PageTest from "./pages/PageTest";
|
import PageTest from "./pages/PageTest";
|
||||||
import CreateTp from "./pages/admin/CreateTp";
|
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 BulkUsers from "./pages/admin/BulkCreateUser";
|
||||||
import Users from "./pages/admin/Users";
|
import Users from "./pages/admin/Users";
|
||||||
|
import Sites from "./pages/admin/Sites";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import AdminPage from "./pages/admin/AdminPage";
|
import AdminPage from "./pages/admin/AdminPage";
|
||||||
@ -21,15 +25,18 @@ function App() {
|
|||||||
<Navigation>
|
<Navigation>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/tps" element={<Practicals />} />
|
<Route path="/immersion" element={<Immersions />} />
|
||||||
<Route path="/tps/:id" element={<Practical />} />
|
<Route path="/immersion/:id" element={<Immersion />} />
|
||||||
|
<Route path="/site/:id" element={<Site />} />
|
||||||
<Route path="/instances" element={<Instances />} />
|
<Route path="/instances" element={<Instances />} />
|
||||||
<Route path="/profile" element={<PageTest />} />
|
<Route path="/profile" element={<PageTest />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="admin" element={<AdminPage />} />
|
<Route path="admin" element={<AdminPage />} />
|
||||||
<Route path="/admin/jdmi" element={<BulkUsers />} />
|
<Route path="/admin/jdmi" element={<BulkUsers />} />
|
||||||
<Route path="/admin/tps" element={<CreateTp />} />
|
<Route path="/admin/tps" element={<CreateTp />} />
|
||||||
|
<Route path="/admin/ji" element={<Jis />} />
|
||||||
<Route path="/admin/users" element={<Users />} />
|
<Route path="/admin/users" element={<Users />} />
|
||||||
|
<Route path="/admin/sites" element={<Sites />} />
|
||||||
<Route path="/settings" element={<CreateTp />} />
|
<Route path="/settings" element={<CreateTp />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const Navigation: React.FC<NavigationProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get("/api/users/me").then((res) => {
|
axios.get("/api/user/me").then((res) => {
|
||||||
if (res.data.username.trim() === "") {
|
if (res.data.username.trim() === "") {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
@ -138,15 +138,28 @@ const Navigation: React.FC<NavigationProps> = ({
|
|||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
to="/admin/tps"
|
to="/admin/ji"
|
||||||
onClick={toggleDrawer}
|
onClick={toggleDrawer}
|
||||||
className="w-60"
|
className="w-60"
|
||||||
>
|
>
|
||||||
Create Tp
|
JDMI
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/admin/sites"
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
className="w-60"
|
||||||
|
>
|
||||||
|
Sites
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
to="/admin/users"
|
to="/admin/users"
|
||||||
@ -183,25 +196,11 @@ const Navigation: React.FC<NavigationProps> = ({
|
|||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<DocumentTextIcon className="size-6" />
|
<DocumentTextIcon className="size-6" />
|
||||||
<Link to="/tps" onClick={toggleDrawer}>
|
<Link to="/immersion" onClick={toggleDrawer}>
|
||||||
TPs
|
Immersions
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,16 +1,25 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Link } from "react-router-dom";
|
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";
|
import { DashboardType } from "../type/Dashboard";
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const [dashboard, setDashboard] = useState<DashboardType | null>(null);
|
const [dashboard, setDashboard] = useState<DashboardType | null>(null);
|
||||||
const username = localStorage.getItem("username");
|
const username = localStorage.getItem("username");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get("/api/dashboard").then((res) => {
|
axios
|
||||||
setDashboard(res.data);
|
.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 (
|
return (
|
||||||
@ -27,22 +36,99 @@ function Dashboard() {
|
|||||||
</section>
|
</section>
|
||||||
{dashboard && (
|
{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">
|
<section className="py-16">
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<h3 className="text-2xl font-bold text-center mb-6">
|
<h3 className="text-2xl font-bold text-center mb-6">
|
||||||
Practicals
|
Immersions - Activites
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<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
|
<div
|
||||||
key={tp.id}
|
key={jiParticipant.id}
|
||||||
className="card card-compact bg-base-200 shadow-lg p-4"
|
className="card card-compact bg-base-200 shadow-lg p-4"
|
||||||
>
|
>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">{tp.name}</h2>
|
<h2 className="card-title">{jiParticipant.name}</h2>
|
||||||
<p>{tp.description}</p>
|
<p>{jiParticipant.description}</p>
|
||||||
<div className="card-actions justify-end">
|
<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
|
Learn More
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -50,52 +136,15 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{dashboard.tps.length === 0 && (
|
{dashboard.jiParticipant.length === 0 && (
|
||||||
<h1 className="text-xl">You have no tps</h1>
|
<div className="container mx-auto text-center">
|
||||||
|
<h1 className="text-xl">
|
||||||
|
You are not registered on any activities.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section className="py-8">
|
||||||
<div className="container mx-auto text-center">
|
<div className="container mx-auto text-center">
|
||||||
<h4 className="text-2xl font-semibold">Messages</h4>
|
<h4 className="text-2xl font-semibold">Messages</h4>
|
||||||
|
|||||||
393
src/pages/Immersion.tsx
Normal file
393
src/pages/Immersion.tsx
Normal 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;
|
||||||
@ -1,32 +1,32 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Tp } from "../type/TpType";
|
import { Ji } from "../type/JiType";
|
||||||
|
|
||||||
function Practicals() {
|
function Immersions() {
|
||||||
const [tps, setTps] = useState([]);
|
const [jis, setJis] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get("/api/tps").then((res) => {
|
axios.get("/api/ji/listall").then((res) => {
|
||||||
setTps(res.data);
|
setJis(res.data);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-10">
|
<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">
|
<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
|
<div
|
||||||
key={practical.id}
|
key={ji.id}
|
||||||
className="card card-compact bg-base-200 shadow-lg p-4"
|
className="card card-compact bg-base-200 shadow-lg p-4"
|
||||||
>
|
>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">{practical.name}</h2>
|
<h2 className="card-title">{ji.name}</h2>
|
||||||
<p>{practical.description}</p>
|
<p>{ji.description}</p>
|
||||||
<div className="card-actions justify-end">
|
<div className="card-actions justify-end">
|
||||||
<Link to={`/tps/${practical.id}`} className="btn btn-primary">
|
<Link to={`/immersion/${ji.id}`} className="btn btn-primary">
|
||||||
Learn More
|
Open activity
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,4 +37,4 @@ function Practicals() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Practicals;
|
export default Immersions;
|
||||||
@ -22,9 +22,9 @@ const LoginPage: React.FC = () => {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
axios.get("/api/users/me").then((res) => {
|
axios.get("/api/user/me").then((res) => {
|
||||||
localStorage.setItem("username", res.data.username);
|
localStorage.setItem("username", res.data.username);
|
||||||
if (res.data.roles.includes("root")) {
|
if (res.data.roles.includes("ROOT")) {
|
||||||
localStorage.setItem("root", "true");
|
localStorage.setItem("root", "true");
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("root");
|
localStorage.removeItem("root");
|
||||||
|
|||||||
@ -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
88
src/pages/Site.tsx
Normal 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;
|
||||||
@ -2,22 +2,22 @@ import axios from "axios";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { BulkUserCreattionType } from "../../type/BulkUserCreattionType";
|
import { BulkUserCreattionType } from "../../type/BulkUserCreattionType";
|
||||||
import { Tp } from "../../type/TpType";
|
import { Ji } from "../../type/TpType";
|
||||||
import { CSVParseType } from "../../type/CSVParseType";
|
import { CSVParseType } from "../../type/CSVParseType";
|
||||||
|
|
||||||
function BulkUsers() {
|
function BulkUsers() {
|
||||||
const [userData, setUserData] = useState<CSVParseType[]>([]);
|
const [userData, setUserData] = useState<CSVParseType[]>([]);
|
||||||
const [practical, setPractical] = useState("0");
|
const [practical, setPractical] = useState("0");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [tps, setTps] = useState([]);
|
const [jis, setJis] = useState([]);
|
||||||
|
|
||||||
const handlePracticalChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handlePracticalChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
setPractical(e.target.value);
|
setPractical(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get("/api/tps").then((res) => {
|
axios.get("/api/ji/listall").then((res) => {
|
||||||
setTps(res.data);
|
setJis(res.data);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -49,14 +49,57 @@ function BulkUsers() {
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
axios
|
try {
|
||||||
.post("/api/users/jdmi", {
|
const response = await axios.post("/api/users", {
|
||||||
users: userData.filter((user: CSVParseType) => user.name !== ""),
|
users: userData.filter((user: CSVParseType) => user.name !== ""),
|
||||||
tpId: practical,
|
jiId: parseInt(practical),
|
||||||
})
|
});
|
||||||
.then(() => navigate(`/admin/users`));
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="max-w-7xl mx-auto p-5">
|
<div className="max-w-7xl mx-auto p-5">
|
||||||
@ -86,11 +129,6 @@ function BulkUsers() {
|
|||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
<td>{user.email}</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -99,7 +137,7 @@ function BulkUsers() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<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
|
<select
|
||||||
name="practical"
|
name="practical"
|
||||||
value={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"
|
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>;
|
return <option value="0">Empty</option>;
|
||||||
{tps.map((tp: Tp) => {
|
{jis.map((ji: Ji) => {
|
||||||
return <option value={tp.id}>{tp.name}</option>;
|
return <option value={ji.id}>{ji.name}</option>;
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="btn btn-primary mt-4"
|
className="btn btn-primary mt-4"
|
||||||
disabled={userData.length === 0}
|
disabled={userData.length === 0 || practical == "0"}
|
||||||
>
|
>
|
||||||
Create Users
|
Create Users
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
200
src/pages/admin/Jis.tsx
Normal file
200
src/pages/admin/Jis.tsx
Normal 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
163
src/pages/admin/Sites.tsx
Normal 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;
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { Instance } from "./InstanceType";
|
import { Ji } from "./JiType";
|
||||||
import { Tp } from "./TpType";
|
import { Sujet } from "./SujetType";
|
||||||
|
|
||||||
export interface DashboardType {
|
export interface DashboardType {
|
||||||
tps: Tp[];
|
jiRespo: Ji[];
|
||||||
instances: Instance[];
|
jiParticipant: Ji[];
|
||||||
|
sujetRespo: Sujet[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { Tp } from "./TpType";
|
|
||||||
import { User } from "./UserType";
|
import { User } from "./UserType";
|
||||||
|
|
||||||
export interface Instance {
|
export interface Instance {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
ssh: string;
|
port: string;
|
||||||
pwd: string;
|
|
||||||
user: User;
|
|
||||||
tp: Tp;
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/type/JiType.ts
Normal file
13
src/type/JiType.ts
Normal 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
9
src/type/SiteType.ts
Normal 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
7
src/type/SujetType.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Sujet {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
pdfLink: string;
|
||||||
|
respos: string;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user