Skip to content

Commit

Permalink
Cleanup messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Aqua-sc committed May 22, 2024
1 parent d6cbe8e commit bf8a8e8
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 102 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ docker.env
startBackend.sh

/.env
backend/web-bff/App/.env
30 changes: 30 additions & 0 deletions frontend/src/components/common/MarkdownTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomTooltipProps> = ({ label, tooltipContent, placement = 'bottom' }) => {

const contentLength = tooltipContent.length;
const calculatedWidth = contentLength > 100 ? "500px" : "auto";

const overlayInnerStyle = { width: calculatedWidth, maxWidth: "75vw", paddingLeft:"12px"};

return (
<Space>
{label}

<Tooltip placement={placement} title={<MarkdownTextfield content={tooltipContent} inTooltip={true} />} overlayInnerStyle={overlayInnerStyle} className='tooltip-markdown'>
<QuestionCircleOutlined style={{ color: 'gray' }} />
</Tooltip>
</Space>
);
};

export default CustomTooltip;
186 changes: 105 additions & 81 deletions frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<boolean>(true)
const [withTemplate, setWithTemplate] = useState<boolean>(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);
Expand All @@ -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 (
<>
<Form.Item
label="Docker image"
label={
<MarkdownTooltip
label={"Docker Image"}
tooltipContent={t("project.tests.dockerImageTooltip")}
placement="right"
/>
}
name="dockerImage"
tooltip={t("project.tests.dockerImageTooltip")} >
>
<Input
style={{ marginTop: "8px" }}
placeholder={t("project.tests.dockerImagePlaceholder")}
Expand All @@ -107,56 +125,87 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => {
<>
<Form.Item
rules={[{ required: !dockerDisabled, message: "Docker script is required" }]}
label="Docker start script"
label={
<MarkdownTooltip
label={"Docker start script"}
tooltipContent={t("project.tests.dockerScriptTooltip")}
placement="right"
/>
}
name="dockerScript"
tooltip={t("project.tests.dockerScriptTooltip")}
>
<Input.TextArea
disabled={dockerDisabled}
autoSize={{ minRows: 3 }}
style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }}
/>
</Form.Item>
<Form.Item
label={
<MarkdownTooltip
label={"Docker test directory"}
tooltipContent={t("project.tests.dockerTestDirTooltip")}
placement="right"
/>
}
name="dockerTestDir"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture"
maxCount={1}
disabled={dockerDisabled}
accept="application/zip, application/x-zip-compressed, application/octet-stream, application/x-zip, *.zip"
beforeUpload={ (file) => {
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
}}
>
<Button disabled={dockerDisabled} icon={<UploadOutlined />}>Upload test directory (zip)</Button>
</Upload>
</Form.Item>
{/* <UploadBtn
form={form}
disabled={dockerDisabled}
fieldName="dockerScript"
/> */}
<div style={{ paddingBottom: '14px' }}>
<div style={{ paddingBottom: '14px'}}>
<Switch
checked={withArtifacts}
checked={withTemplate}
checkedChildren={t("project.tests.templateMode")}
unCheckedChildren={t("project.tests.simpleMode")}
onChange={setWithArtifacts}
onChange={setWithTemplate}
className={switchClassName}
/>
</div>

{withArtifacts ?
{withTemplate ?
<div>
<span style={{ color: 'darkgray', fontSize: '0.8em' }}>{t("project.tests.templateModeInfo")}
<br/>
<br/>
</span>
<MarkdownTextfield content={t("project.tests.templateModeInfo")} />

<Form.Item
label="Template"
label={t("project.tests.dockerTemplate")}
name="dockerTemplate"
tooltip={t("project.tests.dockerTemplateTooltip")}
rules={[
{
validator: (_, value) => {
const errorMessage = isValidTemplate(value)
return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage))
},
},
}, required: true
}
]}
>

<Input.TextArea
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"}
/>
{/*<UploadBtn
form={form}
Expand All @@ -165,35 +214,10 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => {
/>*/}
</Form.Item> </div>: <Form.Item
name="simpleMode"
children={<span style={{color: 'darkgray', fontSize: '0.8em' }}>{t("project.tests.simpleModeInfo")}</span>}
rules={[{ required: true}]}
children={<MarkdownTextfield content={t("project.tests.simpleModeInfo")} />}
rules={[{ required: false}]}
/>}
</>

<Form.Item
label="Docker test directory"
name="dockerTestDir"
tooltip={t("project.tests.dockerTestDirTooltip")}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture"
maxCount={1}
disabled={dockerDisabled}
accept="application/zip, application/x-zip-compressed, application/octet-stream, application/x-zip, *.zip"
beforeUpload={ (file) => {
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
}}
>
<Button disabled={dockerDisabled} icon={<UploadOutlined />}>Upload test directory (zip)</Button>
</Upload>
</Form.Item>
</>
)
}
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Form.Item
label={t("project.change.fileStructure")}
label={
<MarkdownTooltip
label={t("project.tests.fileStructure")}
tooltipContent={t("project.tests.fileStructureTooltip")}
placement="right"
/>
}
name="structureTest"
tooltip={t("project.tests.fileStructureTooltip")}>
>
<Input.TextArea
autoSize={{ minRows: 5 }}
style={{ fontFamily: "monospace" }}
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/input/MarkdownTextfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/pris
import useApp from "../../hooks/useApp"
import { FC } from "react"

const MarkdownTextfield: FC<{ content: string }> = ({ content }) => {
const MarkdownTextfield: FC<{ content: string, inTooltip?: boolean}> = ({ content, inTooltip }) => {
const app = useApp()

const CodeBlock = {
Expand All @@ -29,7 +29,13 @@ const MarkdownTextfield: FC<{ content: string }> = ({ content }) => {
},
}

return <Markdown components={CodeBlock}>{content}</Markdown>
let className = 'markdown-textfield'
console.log(inTooltip)
if (inTooltip) {
className = 'markdown-textfield-intooltip'
}

return <Markdown components={CodeBlock} className={className}>{content}</Markdown>
}

export default MarkdownTextfield
22 changes: 14 additions & 8 deletions frontend/src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit bf8a8e8

Please sign in to comment.