Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Merge pull request #282 from SELab-2/test_page_update
Browse files Browse the repository at this point in the history
Added more clarity and tooltips to test edit page.
  • Loading branch information
Aqua-sc authored May 22, 2024
2 parents 5aac1fb + 5c46196 commit f1fd99b
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 83 deletions.
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;
195 changes: 120 additions & 75 deletions frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { InboxOutlined, UploadOutlined } from "@ant-design/icons"
import { Button, Form, Input, Upload } from "antd"
import {Button, Form, Input, Switch, Upload} from "antd"
import { TextAreaProps } from "antd/es/input"
import { FormInstance } from "antd/lib"
import { FC } from "react"
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,49 +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 [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 @@ -91,12 +97,24 @@ 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="TODO write docs for this"
>
<Input
style={{ marginTop: "8px" }}
Expand All @@ -107,52 +125,30 @@ 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="TODO write docs for this"
>
<Input.TextArea
disabled={dockerDisabled}
autoSize={{ minRows: 3 }}
style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }}
/>
</Form.Item>
{/* <UploadBtn
form={form}
disabled={dockerDisabled}
fieldName="dockerScript"
/> */}

<Form.Item
label="Docker template"
name="dockerTemplate"
tooltip="TODO write docs for this"
rules={[
{
validator: (_, value) => {
const errorMessage = isValidTemplate(value)
return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage))
},
},
]}
>
<Input.TextArea
autoSize={{ minRows: 3 }}
disabled={dockerDisabled}
style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }}
label={
<MarkdownTooltip
label={"Docker test directory"}
tooltipContent={t("project.tests.dockerTestDirTooltip")}
placement="right"
/>
</Form.Item>
{/* <UploadBtn
form={form}
disabled={dockerDisabled}
fieldName="dockerTemplate"
/> */}
</>

<Form.Item
label="Docker test directory"
}
name="dockerTestDir"
tooltip="TODO write docs for this"
valuePropName="fileList"
getValueFromEvent={normFile}
>
Expand All @@ -173,6 +169,55 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => {
<Button disabled={dockerDisabled} icon={<UploadOutlined />}>Upload test directory (zip)</Button>
</Upload>
</Form.Item>
{/* <UploadBtn
form={form}
disabled={dockerDisabled}
fieldName="dockerScript"
/> */}
<div style={{ paddingBottom: '14px'}}>
<Switch
checked={withTemplate}
checkedChildren={t("project.tests.templateMode")}
unCheckedChildren={t("project.tests.simpleMode")}
onChange={setWithTemplate}
className={switchClassName}
/>
</div>

{withTemplate ?
<div>
<MarkdownTextfield content={t("project.tests.templateModeInfo")} />

<Form.Item
label={t("project.tests.dockerTemplate")}
name="dockerTemplate"
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\n@helloUGent\n>optional\nExpected output 2\n"}
/>
{/*<UploadBtn
form={form}
disabled={dockerDisabled}
fieldName="dockerTemplate"
/>*/}
</Form.Item> </div>: <Form.Item
name="simpleMode"
children={<MarkdownTextfield content={t("project.tests.simpleModeInfo")} />}
rules={[{ required: false}]}
/>}
</>
</>
)
}
Expand Down
23 changes: 19 additions & 4 deletions frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
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="TODO write docs for this"
>
<Input.TextArea
autoSize={{ minRows: 3 }}
autoSize={{ minRows: 5 }}
style={{ fontFamily: "monospace" }}
placeholder={
'src/\n' +
' index.js\n' +
' \\\.*\n'+
'common/\n' +
' index.css\n' +
'-node_modules/\n'}
onKeyDown={(e) => {
if (e.key === "Tab") {
e.preventDefault()
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
17 changes: 16 additions & 1 deletion frontend/src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,24 @@
"structureTemplateHeader": "Structure",
"dockerImageHeader": "Docker image",
"dockerScriptHeader": "Docker script",
"dockerTemplate": "Docker template",
"modeHeader": "Template",
"fileStructure": "File structure",
"fileStructurePreview": "File structure preview"
"fileStructurePreview": "File structure preview",
"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.",
"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.\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 f1fd99b

Please sign in to comment.