diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java index 04a4a42f..c71c0bb5 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java @@ -119,7 +119,6 @@ public void testRunDockerTest() throws IOException { verify(dockerModel, times(1)).addZipInputFiles(file); verify(dockerModel, times(1)).cleanUp(); - verify(dockerModel, times(1)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* artifacts are empty */ @@ -128,7 +127,6 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(2)).addZipInputFiles(file); verify(dockerModel, times(2)).cleanUp(); - verify(dockerModel, times(2)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* aritifacts are null */ @@ -137,7 +135,6 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(3)).addZipInputFiles(file); verify(dockerModel, times(3)).cleanUp(); - verify(dockerModel, times(3)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* No template */ @@ -147,7 +144,6 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTestOutput, result); verify(dockerModel, times(4)).addZipInputFiles(file); verify(dockerModel, times(4)).cleanUp(); - verify(dockerModel, times(4)).addUtilFiles(extraFilesPathResolved); /* Error gets thrown */ when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenThrow(new RuntimeException("Error")); diff --git a/frontend/public/docker_langauges/bash.svg b/frontend/public/docker_langauges/bash.svg new file mode 100644 index 00000000..890b5d92 --- /dev/null +++ b/frontend/public/docker_langauges/bash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/custom.svg b/frontend/public/docker_langauges/custom.svg new file mode 100644 index 00000000..c3416414 --- /dev/null +++ b/frontend/public/docker_langauges/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/haskell.svg b/frontend/public/docker_langauges/haskell.svg new file mode 100644 index 00000000..0c627ebd --- /dev/null +++ b/frontend/public/docker_langauges/haskell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/node-js.svg b/frontend/public/docker_langauges/node-js.svg new file mode 100644 index 00000000..3e77c253 --- /dev/null +++ b/frontend/public/docker_langauges/node-js.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/python.svg b/frontend/public/docker_langauges/python.svg new file mode 100644 index 00000000..bf2a1601 --- /dev/null +++ b/frontend/public/docker_langauges/python.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx index edd80af6..f09d02a6 100644 --- a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -1,18 +1,62 @@ -import { UploadOutlined } from "@ant-design/icons" -import {Button, Form, Input, Switch, Upload} from "antd" +import { CodepenCircleFilled, InboxOutlined, UploadOutlined } from "@ant-design/icons" +import { Button, Dropdown, Form, Input, Menu, Select, SelectProps, Switch, Upload } from "antd" +import { TextAreaProps } from "antd/es/input" import { FormInstance } from "antd/lib" -import {FC, useEffect} from "react" +import React, { FC, useEffect, useLayoutEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import useAppApi from "../../../hooks/useAppApi" import MarkdownTooltip from "../../common/MarkdownTooltip" import MarkdownTextfield from "../../input/MarkdownTextfield" +import TextArea from "antd/es/input/TextArea" + +import BashIcon from "../../../../public/docker_langauges/bash.svg" +import PythonIcon from "../../../../public/docker_langauges/python.svg" +import NodeIcon from "../../../../public/docker_langauges/node-js.svg" +import HaskellIcon from "../../../../public/docker_langauges/haskell.svg" +import Custom from "../../../../public/docker_langauges/custom.svg" + + +type DockerLanguage = "bash" | "python" | "node" | "haskell" | "custom" +const languageOptions: Record = { + bash: "fedora", + python: "python", + node: "node", + haskell: "haskell", + custom: "" +} + +const imageToLanguage: Record = { + fedora: "bash", + python: "python", + node: "node", + haskell: "haskell", +} + + +const languagesSelectorItems:SelectProps["options"] = [ + { + label: <>Bash, + value: "bash", + },{ + label: <>Python, + value: "python", + }, { + label: <>NodeJS, + value: "node", + }, { + label: <>Haskell, + value: "haskell", + }, { + label: <>Custom, + value: "custom", + } +] const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() - const {message} = useAppApi() - + const { message } = useAppApi() const dockerImage = Form.useWatch("dockerImage", form) const dockerTemplate = Form.useWatch("dockerTemplate", form) const dockerMode = Form.useWatch("dockerMode", form) @@ -28,6 +72,8 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { form.validateFields(["dockerScript", "dockerTemplate"]) }, [dockerDisabled]) + + const dockerImageSelect= useMemo(()=> imageToLanguage[dockerImage] || "custom",[dockerImage]) function isValidTemplate(template: string): string { if (template.length === 0) { @@ -56,7 +102,7 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { const isDescription = line.length >= 13 && line.substring(0, 13).toLowerCase() === ">description=" // option lines if (line.toLowerCase() !== ">required" && line.toLowerCase() !== ">optional" && !isDescription) { - return t("project.tests.dockerTemplateValidation.inValidOptions", { line:lineNumber.toString() }) + return t("project.tests.dockerTemplateValidation.inValidOptions", { line: lineNumber.toString() }) } } else { isConfigurationLine = false @@ -69,31 +115,22 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { return "" } - - const normFile = (e: any) => { if (Array.isArray(e)) { - return e; + return e } - return e?.fileList; - }; + return e?.fileList + } - let switchClassName = 'template-switch' + let switchClassName = "template-switch" let scriptPlaceholder - + if (withTemplate) { - switchClassName += ' template-switch-active' - scriptPlaceholder = "bash /shared/input/helloworld.sh > \"/shared/output/helloWorldTest\"\n"+ - "bash /shared/input/helloug.sh > \"/shared/output/helloUGent\"\n" + switchClassName += " template-switch-active" + scriptPlaceholder = 'bash /shared/input/helloworld.sh > "/shared/output/helloWorldTest"\n' + 'bash /shared/input/helloug.sh > "/shared/output/helloUGent"\n' } else { - switchClassName += ' template-switch-inactive' - scriptPlaceholder = "output=$(bash /shared/input/helloworld.sh)\n"+ - "if [[ \"$output\" == \"Hello World\" ]]; then \n"+ - " echo 'Test one is successful\n"+ - " echo 'PUSH ALLOWED' > /shared/output/testOutput\n"+ - "else\n"+ - " echo 'Test one failed: script failed to print \"Hello World\"'\n"+ - "fi" + switchClassName += " template-switch-inactive" + scriptPlaceholder = "output=$(bash /shared/input/helloworld.sh)\n" + 'if [[ "$output" == "Hello World" ]]; then \n' + " echo 'Test one is successful\n" + " echo 'PUSH ALLOWED' > /shared/output/testOutput\n" + "else\n" + " echo 'Test one failed: script failed to print \"Hello World\"'\n" + "fi" } @@ -110,11 +147,16 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { name="dockerImage" > form.setFieldValue("dockerImage", languageOptions[val])} + options={languagesSelectorItems} + />} placeholder={t("project.tests.dockerImagePlaceholder")} /> - <> = ({ form }) => { } name="dockerScript" > - @@ -224,4 +266,4 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { ) } -export default DockerFormTab +export default DockerFormTab \ No newline at end of file diff --git a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx index 37d1357d..df5ca7c5 100644 --- a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx @@ -2,106 +2,88 @@ import { DatePicker, Form, FormInstance, Input, Switch, Typography } from "antd" import { useTranslation } from "react-i18next" import { FC } from "react" import MarkdownEditor from "../../input/MarkdownEditor" -import dayjs from 'dayjs'; +import dayjs from "dayjs" const GeneralFormTab: FC<{ form: FormInstance }> = ({ form }) => { - const { t } = useTranslation() - const description = Form.useWatch("description", form) - const visible = Form.useWatch("visible", form) + const { t } = useTranslation() + const description = Form.useWatch("description", form) + const visible = Form.useWatch("visible", form) - return ( - <> - - - + return ( + <> + + + - - {t("project.change.description")} - - + {t("project.change.description")} + - - - + + + - {!visible && ( - - current && current.isBefore(dayjs().startOf('day'))} - /> - - )} + {!visible && ( + + current && current.isBefore(dayjs().startOf("day"))} + /> + + )} - - - + + + - - { - const hours = []; - for (let i = 0; i < dayjs().hour(); i++) { - hours.push(i); - } - return hours; - }, - disabledMinutes: (selectedHour) => { - const minutes = []; - if (selectedHour === dayjs().hour()) { - for (let i = 0; i < dayjs().minute(); i++) { - minutes.push(i); - } - } - return minutes; - }, - disabledSeconds: (selectedHour, selectedMinute) => { - const seconds = []; - if (selectedHour === dayjs().hour() && selectedMinute === dayjs().minute()) { - for (let i = 0; i < dayjs().second(); i++) { - seconds.push(i); - } - } - return seconds; - }, - }} - format="YYYY-MM-DD HH:mm:ss" - disabledDate={(current) => current && current.isBefore(dayjs().startOf('day'))} - - /> - - - ) + + { + const hours = [] + for (let i = 0; i < dayjs().hour(); i++) { + hours.push(i) + } + return hours + }, + }} + format="YYYY-MM-DD HH:mm:ss" + disabledDate={(current) => current && current.isBefore(dayjs().startOf("day"))} + /> + + + ) } export default GeneralFormTab diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 2104c4a9..292d63ff 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -86,8 +86,8 @@ "search": "Search", "emailError": "Please enter a valid email", "emailTooShort": "Email must be at least 3 characters long", - "nameError": "Name must be at least 3 characters long", - "surnameError": "Surname must be at least 3 characters long", + "nameError": "Name or surname must be at least 3 characters long", + "surnameError": "Name or surname must be at least 3 characters long", "searchTutorial": "Enter a name, surname, or email to find users.", "searchTooShort": "The search must be at least 3 characters long", "noUsersFound": "No users found", @@ -213,7 +213,7 @@ "simpleMode": "Without template", "templateMode": "With template", "fileStructureTooltip": "This templates specifies the file structure a submission has to follow.\nIt uses the following syntax:\n* Folders end on `'/'`\n* Use indents to specify files inside a folder\n* Regex can be used\n\t* `'.'` is still a normal `'.'`\n\t* `'\\.'` can be used as regex `'.'`\n* `'-'` at the start of a line specifies a file/folder that is not allowed", - "dockerImageTooltip": "Specify a valid Docker-container from [Docker Hub](https://hub.docker.com/) on which the test script will be run.", + "dockerImageTooltip": "Specify a valid Docker container from [Docker Hub](https://hub.docker.com/) on which the test script will be run. You can also choose a language with a preselected container.", "dockerScriptTooltip": "Bash-script that is executed.\n* The files of the student's submission can be found in `'/shared/input'`\n* Extra files uploaded below can be found in `'/shared/extra'`\n\n More information about the required output depends on the mode and can be found below.", "dockerTemplateTooltip": "To specify specific tests, you need to provide a template. First, enter the test name with '@{test}'. Below this, you can use '>' to provide options such as ('>required', '>optional', '>description'). Everything under these options until the next test or the end of the file is the expected output.", "dockerTestDirTooltip": "Upload additional files needed for the Docker test. These files are available in the folder `'/shared/extra'`.\n\nOnce uploaded u can click the filename to download them again. Uploading a new file will replace the old one.", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index e30e5e17..b5c963aa 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -87,8 +87,8 @@ "search": "Zoeken", "emailError": "Vul een geldig email adres in", "emailTooShort": "Email moet minstens 3 karakters lang zijn", - "nameError": "Naam moet minstens 3 karakters lang zijn", - "surnameError": "Achternaam moet minstens 3 karakters lang zijn", + "nameError": "Naam of achternaam moet minstens 3 karakters lang zijn", + "surnameError": "Naam of achternaam moet minstens 3 karakters lang zijn", "searchTooShort": "Zoekopdracht moet minstens 3 karakters lang zijn", "searchTutorial": "Vul een email adres, naam of achternaam in om gebruikers op te zoeken.", "noUsersFound": "Geen gebruikers gevonden", @@ -215,7 +215,7 @@ "templateMode": "Met sjabloon", "fileStructurePreview": "Voorbeeld van bestandsstructuur", "fileStructureTooltip": "Dit sjabloon specificeert de bestandsstructuur die een indiening moet volgen.\nHet gebruikt de volgende syntax:\n* Mappen eindigen op `'/'`\n* Gebruik inspringing om bestanden binnen een map aan te geven\n* Regex kan worden gebruikt\n\t* `'.'` is nog steeds een normale `'.'`\n\t* `'\\.'` kan worden gebruikt als regex `'.'`\n* `'-'` aan het begin van een regel geeft aan dat een bestand/map niet is toegestaan", - "dockerImageTooltip": "Specificeer een geldige Docker-container van [Docker Hub](https://hub.docker.com/) waarop het testscript zal worden uitgevoerd.", + "dockerImageTooltip": "Specificeer een geldige Docker-container van [Docker Hub](https://hub.docker.com/) waarop het testscript zal worden uitgevoerd. Je kan ook kiezen voor een voorgeconfigureerde programmeertaal met bijhorende container.", "dockerScriptTooltip": "Bash-script dat wordt uitgevoerd.\n* De bestanden van de student zijn indieningen zijn te vinden in `'/shared/input'`\n* Extra bestanden die hieronder zijn geüpload, zijn te vinden in `'/shared/extra'`\n\nMeer informatie over de vereiste uitvoer is afhankelijk van de modus en is hieronder te vinden.", "dockerTemplateTooltip": "Om specifieke tests te definiëren, moet je een sjabloon invoeren. Geef eerst de naam van de test in met '@{test}'. Hieronder kun je met een '>' opties geven zoals ('>required', '>optional', '>description'). Alles onder de opties tot de volgende test of het einde van het bestand is de verwachte output.", "dockerTestDirTooltip": "Upload extra bestanden die nodig zijn voor de dockertest. Deze bestanden zijn beschikbaar in de map `'/shared/extra'`.\n\nAls je een file geüpload hebt kan je deze downloaden door op de bestandsnaam te klikken. Als je een nieuw bestand uploadt zal het oude bestand overschreven worden.", @@ -250,7 +250,7 @@ "getStarted": "Aan de slag", "docs": "Documentatie" }, - + "submission": { "submission": "Indiening", "submittedFiles": "Ingediende bestanden:", diff --git a/frontend/src/pages/editProject/EditProject.tsx b/frontend/src/pages/editProject/EditProject.tsx index cfc75422..7098c253 100644 --- a/frontend/src/pages/editProject/EditProject.tsx +++ b/frontend/src/pages/editProject/EditProject.tsx @@ -43,10 +43,9 @@ const EditProject: React.FC = () => { dockerImage: null, dockerMode: false } + if (response.success) { const tests = response.response.data - console.log(tests) - if (tests.extraFilesName) { const downloadLink = AppRoutes.DOWNLOAD_PROJECT_TESTS.replace(":projectId", projectId).replace(":courseId", courseId!) @@ -75,7 +74,6 @@ const EditProject: React.FC = () => { } form.setFieldsValue(formVals) - setInitialDockerValues(formVals) } @@ -88,6 +86,9 @@ const EditProject: React.FC = () => { const handleCreation = async () => { const values: ProjectFormData & DockerFormData = form.getFieldsValue() + + console.log(values) + if (values.visible) { values.visibleAfter = null } diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 948b189f..ce669205 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react" -import { Form, Input, Spin, Select, Typography } from "antd" +import { Form, Input, Spin, Select, Typography, Space } from "antd" import UserList from "./components/UserList" import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d" import apiCall from "../../util/apiFetch" @@ -10,18 +10,22 @@ import { UserContext } from "../../providers/UserProvider" import useUser from "../../hooks/useUser" export type UsersType = GET_Responses[ApiRoutes.USERS] -type SearchType = "name" | "surname" | "email" +type SearchType = "name" | "email" const ProfileContent = () => { const [users, setUsers] = useState(null) const myself = useUser() const [loading, setLoading] = useState(false) const [form] = Form.useForm() - const searchValue = Form.useWatch("search", form) + const firstSearchValue = Form.useWatch("first", form) + const secondSearchValue = Form.useWatch("second", form) + const searchValue = `${firstSearchValue || ''} ${secondSearchValue || ''}`.trim(); const [debouncedSearchValue] = useDebounceValue(searchValue, 250) const [searchType, setSearchType] = useState("name") const { t } = useTranslation() + const emailRegex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/; + useEffect(() => { onSearch() }, [debouncedSearchValue]) @@ -43,14 +47,56 @@ const ProfileContent = () => { }) } + const [isError, setIsError] = useState(false) + + const checkValidate = () => { + if (searchType === "email") { + if (!emailRegex.test(form.getFieldValue("first"))) { + return false + } else { + return true + } + } else { + const firstValue = form.getFieldValue("first") + const secondValue = form.getFieldValue("second") + const firstValueLength = firstValue ? firstValue.length : 0 + const secondValueLength = secondValue ? secondValue.length : 0 + if (firstValueLength < 3 && secondValueLength < 3) { + console.log("error") + return false + } else { + console.log("no error") + return true + } + } + } + + const validate = () => { + if (!checkValidate()) { + setIsError(true) + } else { + setIsError(false) + } + } + const onSearch = async () => { - const value = form.getFieldValue("search") - if (!value || value.length < 3) return + //validation + if (!checkValidate()) { + return + } + + const firstValue = form.getFieldValue("first") setLoading(true) const params = new URLSearchParams() - params.append(searchType, form.getFieldValue("search")) + if (searchType === "email") { + params.append(searchType, form.getFieldValue("first")) + } else { + const secondValue = form.getFieldValue("second") + if (firstValue) params.append("name", firstValue) + if (secondValue) params.append("surname", secondValue) + } + console.log(params) apiCall.get((ApiRoutes.USERS + "?" + params.toString()) as ApiRoutes.USERS).then((res) => { - setUsers(res.data) setLoading(false) }) @@ -62,49 +108,63 @@ const ProfileContent = () => { form={form} name="search" onFinish={onSearch} + onChange={validate} + validateTrigger={[]} > - + + + setSearchType(value)} - style={{ width: 120 }} - options={[ - { label: t("editRole.email"), value: "email" }, - { label: t("editRole.name"), value: "name" }, - { label: t("editRole.surname"), value: "surname" }, - ]} - /> - } - /> + + {searchType === "name" && ( + + + + )} + + + {isError && {searchType === "email" ? t("editRole.emailError") : t("editRole.nameError")}} +
+ {users !== null ? ( <> {loading ? ( diff --git a/frontend/src/pages/editRole/components/UserList.tsx b/frontend/src/pages/editRole/components/UserList.tsx index ed4b77bf..c62e5c55 100644 --- a/frontend/src/pages/editRole/components/UserList.tsx +++ b/frontend/src/pages/editRole/components/UserList.tsx @@ -6,6 +6,7 @@ import { useState } from "react" import { UsersType } from "../EditRole" import { GET_Responses, ApiRoutes } from "../../../@types/requests.d" import { User } from "../../../providers/UserProvider" +import useUser from "../../../hooks/useUser" //this is ugly, but if I put this in GET_responses, it will be confused with the User type (and there's no GET request with this as a response). //this is also the only place this is used, so I think it's fine. @@ -16,9 +17,10 @@ const UserList: React.FC<{ users: UsersType; updateRole: (user: UsersListItem, r const [visible, setVisible] = useState(false) const [selectedUser, setSelectedUser] = useState(null) const [selectedRole, setSelectedRole] = useState(null) + const { user } = useUser() - const handleMenuClick = (user: UsersListItem, role: UserRole) => { - setSelectedUser(user) + const handleMenuClick = (listuser: UsersListItem, role: UserRole) => { + setSelectedUser(listuser) setSelectedRole(role) setVisible(true) } @@ -44,12 +46,13 @@ const UserList: React.FC<{ users: UsersType; updateRole: (user: UsersListItem, r return a.email.localeCompare(b.email); }); - const renderUserItem = (user: UsersListItem) => ( + const renderUserItem = (listuser: UsersListItem) => ( - + handleMenuClick(user, e.key as UserRole), + selectedKeys: [listuser.role], + onClick: (e) => handleMenuClick(listuser, e.key as UserRole), }} > e.preventDefault()}> - {t("editRole." + user.role)} + {t("editRole." + listuser.role)} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 8d61f9c2..9c426dcb 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -181,6 +181,13 @@ html { } +.select-icon { + width: 1em; + margin-right: 0.5rem; + position: relative; + top: 2px; +} + /* *************************** Landing page *************************** */ .landing-page * {