Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more clarity and tooltips to test edit page. #282

Merged
merged 4 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
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
Loading