From bf8a8e8462ddbf21ef9bd13d9e1f9bf4c1040f5a Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Wed, 22 May 2024 15:51:01 +0200 Subject: [PATCH] Cleanup messages --- .gitignore | 1 + .../src/components/common/MarkdownTooltip.tsx | 30 +++ .../forms/projectFormTabs/DockerFormTab.tsx | 186 ++++++++++-------- .../projectFormTabs/StructureFormTab.tsx | 15 +- .../components/input/MarkdownTextfield.tsx | 10 +- frontend/src/i18n/en/translation.json | 22 ++- frontend/src/i18n/nl/translation.json | 22 ++- frontend/src/styles.css | 61 ++++++ 8 files changed, 245 insertions(+), 102 deletions(-) create mode 100644 frontend/src/components/common/MarkdownTooltip.tsx diff --git a/.gitignore b/.gitignore index 9a182e36..87feac74 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ docker.env startBackend.sh /.env +backend/web-bff/App/.env diff --git a/frontend/src/components/common/MarkdownTooltip.tsx b/frontend/src/components/common/MarkdownTooltip.tsx new file mode 100644 index 00000000..89ca83ed --- /dev/null +++ b/frontend/src/components/common/MarkdownTooltip.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Space, Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import MarkdownTextfield from '../input/MarkdownTextfield'; + +interface CustomTooltipProps { + label: string; + tooltipContent: string; + placement?: 'top' | 'left' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom'; +} + +const CustomTooltip: React.FC = ({ label, tooltipContent, placement = 'bottom' }) => { + + const contentLength = tooltipContent.length; + const calculatedWidth = contentLength > 100 ? "500px" : "auto"; + + const overlayInnerStyle = { width: calculatedWidth, maxWidth: "75vw", paddingLeft:"12px"}; + + return ( + + {label} + + } overlayInnerStyle={overlayInnerStyle} className='tooltip-markdown'> + + + + ); +}; + +export default CustomTooltip; diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx index 65bfa88e..c28ac625 100644 --- a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -6,6 +6,9 @@ import {FC, useState} from "react" import { useTranslation } from "react-i18next" import { ApiRoutes } from "../../../@types/requests" import useAppApi from "../../../hooks/useAppApi" +import MarkdownTooltip from "../../common/MarkdownTooltip" +import { classicNameResolver } from "typescript" +import MarkdownTextfield from "../../input/MarkdownTextfield" const UploadBtn: React.FC<{ form: FormInstance; fieldName: string; textFieldProps?: TextAreaProps; disabled?: boolean }> = ({ form, fieldName, disabled }) => { const handleFileUpload = (file: File) => { @@ -39,50 +42,52 @@ const UploadBtn: React.FC<{ form: FormInstance; fieldName: string; textFieldProp ) } -function isValidTemplate(template: string): string { - if (!template?.length) return "" // Template is optional - let atLeastOne = false // Template should not be empty - const lines = template.split("\n") - if (lines[0].charAt(0) !== "@") { - return 'Error: The first character of the first line should be "@"' - } - let isConfigurationLine = false - for (const line of lines) { - if (line.length === 0) { - // skip line if empty - continue - } - if (line.charAt(0) === "@") { - atLeastOne = true - isConfigurationLine = true - continue - } - if (isConfigurationLine) { - if (line.charAt(0) === ">") { - const isDescription = line.length >= 13 && line.substring(0, 13).toLowerCase() === ">description=" - // option lines - if (line.toLowerCase() !== ">required" && line.toLowerCase() !== ">optional" && !isDescription) { - return 'Error: Option lines should be either ">Required", ">Optional" or start with ">Description="' - } - } else { - isConfigurationLine = false - } - } - } - if (!atLeastOne) { - return "Error: Template should not be empty" - } - return "" -} - const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() const {message} = useAppApi() - const [withArtifacts, setWithArtifacts] = useState(true) + const [withTemplate, setWithTemplate] = useState(true) const dockerImage = Form.useWatch("dockerImage", form) const dockerDisabled = !dockerImage?.length + function isValidTemplate(template: string): string { + if (!template?.length) return "" // Template is optional + let atLeastOne = false // Template should not be empty + const lines = template.split("\n") + if (lines[0].charAt(0) !== "@") { + return t("project.tests.dockerTemplateValidation.inValidFirstLine") + } + let isConfigurationLine = false + let lineNumber = 0 + for (const line of lines) { + lineNumber++ + if (line.length === 0) { + // skip line if empty + continue + } + if (line.charAt(0) === "@") { + atLeastOne = true + isConfigurationLine = true + continue + } + if (isConfigurationLine) { + if (line.charAt(0) === ">") { + 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() }) + } + } else { + isConfigurationLine = false + } + } + } + if (!atLeastOne) { + return t("project.tests.dockerTemplateValidation.emptyTemplate") + } + return "" + } + const normFile = (e: any) => { console.log('Upload event:', e); @@ -92,12 +97,25 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { return e?.fileList; }; + let switchClassName = 'template-switch' + if (withTemplate) { + switchClassName += ' template-switch-active' + } else { + switchClassName += ' template-switch-inactive' + } + return ( <> + } name="dockerImage" - tooltip={t("project.tests.dockerImageTooltip")} > + > = ({ form }) => { <> + } name="dockerScript" - tooltip={t("project.tests.dockerScriptTooltip")} > = ({ form }) => { style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }} /> + + } + name="dockerTestDir" + valuePropName="fileList" + getValueFromEvent={normFile} + > + { + const isZIP = file.type.includes('zip') || file.name.includes('.zip') + if (!isZIP) { + message.error(`${file.name} is not a zip file`); + return Upload.LIST_IGNORE + } + return false + }} + > + + + {/* */} -
+
- {withArtifacts ? + {withTemplate ?
- {t("project.tests.templateModeInfo")} -
-
-
+ { const errorMessage = isValidTemplate(value) return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage)) - }, - }, + }, required: true + } ]} > @@ -156,7 +205,7 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { autoSize={{minRows: 4}} disabled={dockerDisabled} style={{fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto"}} - placeholder={"@helloWorldTest\n>required\n>description=\"This is a test\"\nExpected output 1\n@helloUGent\n>optional\nExpected output 2\n"} + placeholder={"@helloWorldTest\n>required\n>description=\"This is a test\"\nExpected output 1\n\n@helloUGent\n>optional\nExpected output 2\n"} /> {/* = ({ form }) => { />*/}
: {t("project.tests.simpleModeInfo")}} - rules={[{ required: true}]} + children={} + rules={[{ required: false}]} />} - - - { - const isZIP = file.type.includes('zip') || file.name.includes('.zip') - if (!isZIP) { - message.error(`${file.name} is not a zip file`); - return Upload.LIST_IGNORE - } - return false - }} - > - - - ) } diff --git a/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx b/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx index 17aa232f..43cf0d0c 100644 --- a/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx @@ -1,21 +1,30 @@ -import { Form, Input, Typography } from "antd" +import { Form, Input, Typography, Tooltip, Space } from "antd" +import { QuestionCircleOutlined } from "@ant-design/icons" import { FC } from "react" import SubmitStructure from "../../../pages/submit/components/SubmitStructure" import { useTranslation } from "react-i18next" import { FormInstance } from "antd/lib" import { useDebounceValue } from "usehooks-ts" +import MarkdownTooltip from "../../common/MarkdownTooltip" const StructureFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() const structure = Form.useWatch("structureTest", form) const [debouncedValue] = useDebounceValue(structure, 400) + return ( <> + } name="structureTest" - tooltip={t("project.tests.fileStructureTooltip")}> + > = ({ content }) => { +const MarkdownTextfield: FC<{ content: string, inTooltip?: boolean}> = ({ content, inTooltip }) => { const app = useApp() const CodeBlock = { @@ -29,7 +29,13 @@ const MarkdownTextfield: FC<{ content: string }> = ({ content }) => { }, } - return {content} + let className = 'markdown-textfield' + console.log(inTooltip) + if (inTooltip) { + className = 'markdown-textfield-intooltip' + } + + return {content} } export default MarkdownTextfield diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 116fa82e..a0c290fc 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -204,18 +204,24 @@ "structureTemplateHeader": "Structure", "dockerImageHeader": "Docker image", "dockerScriptHeader": "Docker script", + "dockerTemplate": "Docker template", "modeHeader": "Template", "fileStructure": "File structure", "fileStructurePreview": "File structure preview", - "simpleMode": "Simple mode", - "templateMode": "Template mode", - "fileStructureTooltip": "Describe the project structure with a simple template, which is indentation-sensitive. Each filename is in regex, where the dot is converted to a (non-)escaped dot. Under each directory, describe the files in that directory by listing them on the following lines with the same indentation. To specify that it is a directory, end the filename with a '/'. You can also blacklist files by prefixing the filename with a '-'.", - "dockerImageTooltip": "Enter the Docker image that will be used to run the container. This must be a valid image available on Docker Hub.", - "dockerScriptTooltip": "The Docker script is the script that will be executed when starting a container with the above image. This script is always executed in bash on the container. You can also upload a script. This script has access to the files in /shared/input, where the student's files are located.", + "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 idents 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.", + "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": "Here you can upload additional test utility files that will be available in the container. They will be located in the /shared/extra folder.", - "simpleModeInfo": "In simple mode, the container will execute the Docker script. Everything logged to the console will be visible to the student as feedback. This allows you to provide feedback to the student using print statements. To specify whether the submission was successful or not, you must write 'Push allowed/denied' to the file /shared/output/dockerOutput.", - "templateModeInfo": "In template mode, the student receives feedback in a more structured format. You can specify multiple small subtests, and the student will see the differences between their output and the expected output in these tests." + "dockerTestDirTooltip": "Upload additional files needed for the Docker test.\n\nThese files are available in the folder `'/shared/extra'`.", + "simpleModeInfo": "Without template, the student will see everything that the scripts prints/logs as feedback.\n\nIf the test is successful, `'Push allowed'` must be written to `'/shared/output/testOutput'`. If this does not happen, the test is considered failed.", + "templateModeInfo": "If you provide a template, the student will see a comparison between the expected output and the student's output for each test.\n\nThe template uses the following syntax:\n* `@testName`: the first line of a test starts with `'@'` followed by the name of the test\n* Optionally, a number of options can be provided:\n\t* `>[required|optional]`: indicates whether the test is mandatory or optional\n\t* `>description=\"...\"`: description of the test\n* The lines after the options are the expected output of the test. The last newline is not considered part of the output.", + "dockerTemplateValidation": { + "inValidFirstLine": "The first line of a test must be '@' followed by the name of the test", + "inValidOptions": "Line {{line}}: Invalid option", + "emptyTemplate": "Template cannot be empty" + } }, "noScore": "No score available", "noFeedback": "No feedback provided", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 9a510398..7c68cff0 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -206,18 +206,24 @@ "structureTemplateHeader": "Structuur", "dockerImageHeader": "Docker image", "dockerScriptHeader": "Docker script", + "dockerTemplate": "Docker sjabloon", "modeHeader": "Sjabloon", "fileStructure": "Bestandsstructuur", - "simpleMode": "Simpele modus", - "templateMode": "Template modus", + "simpleMode": "Zonder sjabloon", + "templateMode": "Met sjabloon", "fileStructurePreview": "Voorbeeld van bestandsstructuur", - "fileStructureTooltip": "Beschrijf de projectstructuur met een eenvoudig sjabloon, dat gevoelig is voor inspringing. Elke bestandsnaam is in regex, waarbij de punt wordt omgezet naar een (niet)geëscapete punt. Onder elke map beschrijf je de bestanden in deze map, door ze op de volgende regels te zetten met dezelfde inspringing. Om aan te geven dat het een map is, eindig je de bestandsnaam met een '/'. Je kunt ook bestanden uitsluiten door een '-' voor de bestandsnaam te zetten.", - "dockerImageTooltip": "Voer de Docker image in die zal worden gebruikt om de container uit te voeren. Dit moet een geldige image zijn die beschikbaar is op Docker Hub.", - "dockerScriptTooltip": "Het Docker script is het script dat wordt uitgevoerd bij het starten van een container met bovenstaande image. Dit script wordt altijd uitgevoerd in bash op de container. Je kunt ook een script uploaden. Dit script heeft toegang tot de bestanden in /shared/input, waar de student zijn bestanden kan plaatsen.", + "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 normaal `'.'`\n\t* `'\\.'` kan worden gebruikt als regex `'.'`\n* `'-'` aan het begin van een regel geeftaan 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.", + "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": "Hier kun je extra test utility bestanden uploaden die beschikbaar zullen zijn in de container. Ze zullen worden geplaatst in de map /shared/extra.", - "simpleModeInfo": "In de eenvoudige modus zal de container het Docker script uitvoeren. Alles wat naar de console wordt gelogd, is zichtbaar voor de student als feedback. Zo kun je zelf met printstatements feedback geven aan de student. Om aan te geven of de indiening is geslaagd, moet je 'Push allowed/denied' schrijven naar het bestand /shared/output/dockerOutput.", - "templateModeInfo": "In de sjabloonmodus krijgt de student gestructureerde feedback te zien. Je kunt meerdere kleine subtests opgeven, en de student ziet wat het verschil is met het verwachtte resultaat in deze tests." + "dockerTestDirTooltip": "Upload extra bestanden die nodig zijn voor de dockertest.\n\nDeze bestanden zijn beschikbaar in de map `'/shared/extra'`.", + "simpleModeInfo": "Zonder sjabloon zal de student alles zien wat de scripts print/logt als feedback.\n\nAls de test geslaagd is moet er `'Push allowed'` naar `'/shared/output/testOutput'` geschreven worden. Als dit niet gebeurt, wordt de test beschouwd als mislukt.", + "templateModeInfo": "Als je een sjabloon meegeeft krijgt de student per test een vergelijking te zien tussen de verwachte output en de output van de student.\n\nHet sjabloon gebruikt volgende syntax:\n* `@testNaam`: eerste lijn van een test begint met `'@'` en de naam van de test\n* Optioneel kunnen een aantal opties meegegeven worden:\n\t* `>[required|optional]`: geeft aan of de test verplicht of optioneel is\n\t* `>description=\"...\"`: beschrijving van de test\n* De regels na de opties zijn de verwachte output van de test. De laatste newline wordt niet gezien als deel van de output.", + "dockerTemplateValidation": { + "inValidFirstLine": "De eerste lijn van een regel moet beginnen met '@' gevolgd door de naam van de test", + "inValidOptions": "Lijn {{line}}: Ongeldige optie", + "emptyTemplate": "Template kan niet leeg zijn" + } }, "noScore": "Nog geen score beschikbaar", "noFeedback": "Geen feedback gegeven", diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f5592199..ca203195 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -71,6 +71,66 @@ html { } +.template-switch { + height: 30px; + display: flex; + align-items: center; + width: 100%; + justify-content: center; +} + +.template-switch .ant-switch-inner { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.template-switch-active .ant-switch-inner-checked{ + font-size: 16px !important; +} + +.template-switch-inactive .ant-switch-inner-checked{ + + font-size: 0px !important; +} + +.template-switch-inactive .ant-switch-inner-unchecked{ + margin-top: 2px !important; + font-size: 16px !important; +} + +.template-switch-active .ant-switch-inner-unchecked{ + width: 0px !important; + font-size: 0px !important; +} + + + + +.template-switch-active .ant-switch-handle { + height: 20px; + width: 20px; + margin-top: 3px; + margin-right: 3px; + margin-left: -5px +} + +.template-switch-inactive .ant-switch-handle { + height: 20px; + width: 20px; + margin-top: 3px; + margin-right: -5px; + margin-left: 3px +} + + + + +.markdown-textfield-intooltip a { + color: #1890ff; +} + @font-face { font-family: 'JetBrains Mono'; src: url('\theme\fonts\JetBrainsMono-Regular.ttf') format('truetype'); @@ -252,6 +312,7 @@ html { + @media screen and (max-width: 600px) { .nav-logo { display: none;