diff --git a/package.json b/package.json index 5d9c6dd..a894707 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/src/App.tsx b/src/App.tsx index 6e8521e..287bcb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,14 +2,18 @@ import "./App.css"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import Navigation from "./component/Navigation/Navigation"; import Dashboard from "./pages/Dashboard"; -import Practicals from "./pages/Practicals"; +import Immersions from "./pages/Immersions"; import Instances from "./pages/Instances"; -import Practical from "./pages/Practical"; +import Immersion from "./pages/Immersion"; +import Site from "./pages/Site"; import LoginPage from "./pages/Login"; import PageTest from "./pages/PageTest"; import CreateTp from "./pages/admin/CreateTp"; +import CreateSite from "./pages/admin/CreateSite"; +import Jis from "./pages/admin/Jis"; import BulkUsers from "./pages/admin/BulkCreateUser"; import Users from "./pages/admin/Users"; +import Sites from "./pages/admin/Sites"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import AdminPage from "./pages/admin/AdminPage"; @@ -21,15 +25,18 @@ function App() { } /> - } /> - } /> + } /> + } /> + } /> } /> } /> } /> } /> } /> } /> + } /> } /> + } /> } /> diff --git a/src/component/Navigation/Navigation.tsx b/src/component/Navigation/Navigation.tsx index 114416d..b4ede85 100644 --- a/src/component/Navigation/Navigation.tsx +++ b/src/component/Navigation/Navigation.tsx @@ -41,7 +41,7 @@ const Navigation: React.FC = ({ }; useEffect(() => { - axios.get("/api/users/me").then((res) => { + axios.get("/api/user/me").then((res) => { if (res.data.username.trim() === "") { navigate("/login"); } @@ -138,15 +138,28 @@ const Navigation: React.FC = ({
  • - Create Tp + JDMI
  • +
  • +
    + + Sites + +
    +
  • + +
  • = ({
  • - - TPs + + Immersions
  • -
  • -
    - - - Instances - -
    -
  • -
  • -
    - - Messages -
    -
  • )} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index a3839a6..fbc0309 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,16 +1,25 @@ import { useEffect, useState } from "react"; import axios from "axios"; import { Link } from "react-router-dom"; -import { Tp } from "../type/TpType"; +import { Sujet } from "../type/SujetType"; +import { Ji } from "../type/JiType"; import { DashboardType } from "../type/Dashboard"; function Dashboard() { const [dashboard, setDashboard] = useState(null); const username = localStorage.getItem("username"); useEffect(() => { - axios.get("/api/dashboard").then((res) => { - setDashboard(res.data); - }); + axios + .get("/api/dashboard") + .then((res) => { + setDashboard(res.data); + }) + .catch((err) => { + console.log(err); + if (err.response.status === 401 || err.response.status === 403) { + window.location.href = "/login"; + } + }); }, []); return ( @@ -27,22 +36,99 @@ function Dashboard() { {dashboard && ( <> + {dashboard.jiRespo.length !== 0 && ( +
    +
    +

    + Immersions - Respo +

    +
    + {dashboard.jiRespo.map((jiRespo: Ji) => ( +
    +
    +

    {jiRespo.name}

    +

    {jiRespo.description}

    +
    + + GO + +
    +
    +
    + ))} +
    +
    +
    + )} + {dashboard.sujetRespo.length !== 0 && ( +
    +
    +

    + Sujets - Respo +

    +
    + {dashboard.sujetRespo.map((sujet: Sujet) => ( +
    +
    +

    {sujetRespo.name}

    + {sujetRespo && ( +

    + Linked to: {sujetRespo.name} +

    + )} +
    + {false && ( + + Learn More + + )} + {sujetRespo && ( + + See TP + + )} +
    +
    +
    + ))} +
    +
    +
    + )}

    - Practicals + Immersions - Activites

    - {dashboard.tps.map((tp: Tp) => ( + {dashboard.jiParticipant.map((jiParticipant: Ji) => (
    -

    {tp.name}

    -

    {tp.description}

    +

    {jiParticipant.name}

    +

    {jiParticipant.description}

    - + Learn More
    @@ -50,52 +136,15 @@ function Dashboard() {
    ))}
    - {dashboard.tps.length === 0 && ( -

    You have no tps

    + {dashboard.jiParticipant.length === 0 && ( +
    +

    + You are not registered on any activities. +

    +
    )}
    -
    -
    -

    Instances

    -
    - {dashboard.instances.map((instance) => ( -
    -
    -

    {instance.name}

    - {instance.tp && ( -

    Linked to: {instance.tp.name}

    - )} -
    - {false && ( - - Learn More - - )} - {instance.tp && ( - - See TP - - )} -
    -
    -
    - ))} - {dashboard.instances.length === 0 && ( -

    You have no instances

    - )} -
    -
    -

    Messages

    diff --git a/src/pages/Immersion.tsx b/src/pages/Immersion.tsx new file mode 100644 index 0000000..5b602e4 --- /dev/null +++ b/src/pages/Immersion.tsx @@ -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(); + const [instance, setInstance] = useState(); + const [allInstances, setAllInstances] = useState([]); + const [instancesStatus, setInstancesStatus] = useState>({}); + const [containerStatus, setContainerStatus] = useState(""); + const [instancesOwner, setInstancesOwner] = useState(""); + 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 && ( + <> +

    {ji.name}

    +
    + {/* Section supérieure : Info + iframe */} +
    + {/* Colonne gauche - Informations */} +
    +
    +

    + {ji.name} - Information +

    +
      +
    • {ji.description}
    • +
    • + Description: + {ji.description} +
    • +
    • + Date: + {ji.date} +
    • +
    +
    + + {instance && ( +
    +

    + Your credentials : +

    +
      +
    • + SSH: +
      copyText(`ssh -p ${instance.port} ${username}@la-banquise.fr`)} + > + ssh -p {instance.port} {username}@la-banquise.fr +
      + +
      +
      +
    • +
    • + Instance name: +
      + {instance.name} +
      +
    • +
    • + Password: +
      copyText(instance.password)} + > + {instance.password} +
      + +
      +
      +
    • +
    • + Status: +
      + {containerStatus} +
      +
    • +
    +
    + )} + + +
    + + {/* Colonne droite - iframe du sujet */} +
    +
    +

    + Subject +

    + -
    -
    - -
    -
    -

    - Information -

    -
      -
    • {tp.description}
    • -
    • - Duration: - 2 hours -
    • -
    • - Tools Used: - Python, SSH, Vim -
    • -
    • - Difficulty: - Beginner -
    • -
    • - Date: - Oct 23, 2024 -
    • -
    -
    -
    -

    - Resources -

    -
      -
    • - SSH: -
      copyText(tp.ssh)} - > - {tp.ssh} -
      - -
      -
      -
    • -
    • - Port: -
      copyText(tp.port)} - > - {tp.port} -
      - -
      -
      -
    • -
    • - Password: -
      copyText(tp.pwd)} - > - {tp.pwd} -
      - -
      -
      -
    • -
    -
    - -
    -
    -
    - Subject -
    -

    Download

    -
    - -
    -
    -
    -
    -
    - - )} - - ); -} - -export default Practical; diff --git a/src/pages/Site.tsx b/src/pages/Site.tsx new file mode 100644 index 0000000..1e7d1c6 --- /dev/null +++ b/src/pages/Site.tsx @@ -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(); + + 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 && ( + <> +

    {site.name}

    +
    +
    +
    +
    + {site.listJi} +
    +
    + +
    +
    +

    + {site.name} - Informations +

    +
      +
    • + Description: + {site.description} +
    • +
    • + Address: + {site.address} +
    • +
    +
    +
    +

    + Responsables du site +

    + mettre une liste des respo site +
      + +
    +
    + +
    +
    +
    + Subject +
    +

    Download

    +
    + +
    +
    +
    +
    +
    + + )} + + ); +} + +export default Site; diff --git a/src/pages/admin/BulkCreateUser.tsx b/src/pages/admin/BulkCreateUser.tsx index 420f6d8..23d27fd 100644 --- a/src/pages/admin/BulkCreateUser.tsx +++ b/src/pages/admin/BulkCreateUser.tsx @@ -2,22 +2,22 @@ import axios from "axios"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { BulkUserCreattionType } from "../../type/BulkUserCreattionType"; -import { Tp } from "../../type/TpType"; +import { Ji } from "../../type/TpType"; import { CSVParseType } from "../../type/CSVParseType"; function BulkUsers() { const [userData, setUserData] = useState([]); const [practical, setPractical] = useState("0"); const navigate = useNavigate(); - const [tps, setTps] = useState([]); + const [jis, setJis] = useState([]); const handlePracticalChange = (e: React.ChangeEvent) => { setPractical(e.target.value); }; useEffect(() => { - axios.get("/api/tps").then((res) => { - setTps(res.data); + axios.get("/api/ji/listall").then((res) => { + setJis(res.data); }); }, []); @@ -49,14 +49,57 @@ function BulkUsers() { reader.readAsText(file); }; - const handleSubmit = () => { - axios - .post("/api/users/jdmi", { + const handleSubmit = async () => { + try { + const response = await axios.post("/api/users", { users: userData.filter((user: CSVParseType) => user.name !== ""), - tpId: practical, - }) - .then(() => navigate(`/admin/users`)); + jiId: parseInt(practical), + }); + + if (response.status === 200) { + // REMPLACER l'alert par ce code : + let str = ""; + for (let i = 0; i < response.data.successMails.length; i++) { + const mail = response.data.successMails[i]; + const password = response.data.successPasswd[i]; + str += `${mail},${password}\n`; + } + const url = window.URL.createObjectURL( + new Blob([str], { type: "text/csv" }), + ); + const link = document.createElement("a"); + link.href = url; + link.setAttribute( + "download", + `created_users_${new Date().toISOString().split("T")[0]}.csv`, + ); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + alert("Users succesfully created ! CSV downloaded."); + navigate("/admin/users"); + } + if (response.status === 202) + { const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `created_users_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + + alert(`Couldn't create some users`);} + if (response.status === 500) + alert(`Couldn't create ANY users`); + } catch (error) { + console.error('Erreur:', error); + alert('Erreur lors de la création des users'); }; + }; return (
    @@ -86,11 +129,6 @@ function BulkUsers() { {user.name} {user.email} - {user.password} - {user.instance_name} - {user.instance_ssh} - {user.instance_port} - {user.instance_pwd} ))} @@ -99,7 +137,7 @@ function BulkUsers() { )}
    - +
    diff --git a/src/pages/admin/Jis.tsx b/src/pages/admin/Jis.tsx new file mode 100644 index 0000000..ae06430 --- /dev/null +++ b/src/pages/admin/Jis.tsx @@ -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 + ) => { + const { name, value } = e.target; + setNewJi({ ...newJi, [name]: value }); + }; + + const handleSubmit = (e: React.FormEvent) => { + 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 ( +
    +
    +
    +

    + Manage JIs +

    + +
    + + {/* Create JI Form */} + {showCreateForm && ( +
    +

    + Create New JI +

    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + )} + + {/* JIs Table */} +
    + + + + + + + + + + + + + {jis.map((ji: Ji, index: number) => ( + + + + + + + + + ))} + +
    #NameResponsableSite IDDateActions
    {index + 1}{ji.name}{ji.respo}{ji.site_id}{ji.date} + +
    +
    +
    +
    + ); +} + +export default Jis; diff --git a/src/pages/admin/Sites.tsx b/src/pages/admin/Sites.tsx new file mode 100644 index 0000000..e61dab0 --- /dev/null +++ b/src/pages/admin/Sites.tsx @@ -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 ( +
    +
    +
    +

    + Manage Sites +

    + +
    + + {/* Create Site Form */} + {showCreateForm && ( +
    +

    + Create New Site +

    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + )} + + {/* Sites Table */} +
    + + + + + + + + + + + + {sites.map((site, index) => ( + + + + + + + + ))} + +
    #NameAddressRolesActions
    {index + 1}{site.name}{site.address}{site.listJi} + +
    +
    +
    +
    + ); +} + +export default Sites; diff --git a/src/type/Dashboard.ts b/src/type/Dashboard.ts index 18c3925..2abf5e0 100644 --- a/src/type/Dashboard.ts +++ b/src/type/Dashboard.ts @@ -1,7 +1,8 @@ -import { Instance } from "./InstanceType"; -import { Tp } from "./TpType"; +import { Ji } from "./JiType"; +import { Sujet } from "./SujetType"; export interface DashboardType { - tps: Tp[]; - instances: Instance[]; + jiRespo: Ji[]; + jiParticipant: Ji[]; + sujetRespo: Sujet[]; } diff --git a/src/type/InstanceType.ts b/src/type/InstanceType.ts index 2de6819..26eb017 100644 --- a/src/type/InstanceType.ts +++ b/src/type/InstanceType.ts @@ -1,11 +1,7 @@ -import { Tp } from "./TpType"; import { User } from "./UserType"; export interface Instance { id: number; name: string; - ssh: string; - pwd: string; - user: User; - tp: Tp; + port: string; } diff --git a/src/type/JiType.ts b/src/type/JiType.ts new file mode 100644 index 0000000..1387569 --- /dev/null +++ b/src/type/JiType.ts @@ -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[]; +} diff --git a/src/type/SiteType.ts b/src/type/SiteType.ts new file mode 100644 index 0000000..9ca856e --- /dev/null +++ b/src/type/SiteType.ts @@ -0,0 +1,9 @@ +import { Ji } from "./JiType"; + +export interface Site { + id: number; + name: string; + description: string; + address: string; + listJi: List; +} diff --git a/src/type/SujetType.ts b/src/type/SujetType.ts new file mode 100644 index 0000000..0f404ad --- /dev/null +++ b/src/type/SujetType.ts @@ -0,0 +1,7 @@ +export interface Sujet { + id: number; + name: string; + description: string; + pdfLink: string; + respos: string; +} diff --git a/src/type/TpType.ts b/src/type/TpType.ts deleted file mode 100644 index 568d5f7..0000000 --- a/src/type/TpType.ts +++ /dev/null @@ -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; -}