ui: new judge config (#533)

Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
undefinedandpanda authored and GitHub committed 2023-03-15 21:15:04 +08:00
1 parent f4d8cd7a5d
commit 85cb1d6dff
26 files changed
+910 -222

No files matched your search

@@ -0,0 +1,88 @@
import {
Button, ControlGroup, Dialog, DialogBody, DialogFooter, Icon, InputGroup,
} from '@blueprintjs/core';
import { readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import React, { useEffect } from 'react';
import { useSelector, useStore } from 'react-redux';
import { RootState } from '../reducer';
export function AddTestcase() {
const [open, setOpen] = React.useState(false);
const [input, setInput] = React.useState('');
const [output, setOutput] = React.useState('');
const [valid, setValid] = React.useState(false);
const testdata = useSelector((state: RootState) => state.testdata);
const store = useStore<RootState>();
useEffect(() => {
setValid(testdata.find((i) => i.name === input) && testdata.find((i) => i.name === output));
}, [input, output]);
function onConfirm() {
store.dispatch({
type: 'problemconfig/addTestcases',
cases: [{ input, output }],
});
setOpen(false);
}
function auto() {
const state = store.getState();
const subtasks = readSubtasksFromFiles(state.testdata.map((i) => i.name), {});
const current = state.config.subtasks.flatMap((i) => i.cases).concat(state.config.__cases);
const pending = [];
for (const c of subtasks.flatMap((s) => s.cases)) {
if (!current.find((i) => i.input === c.input && i.output === c.output)) {
pending.push({
input: c.input,
output: c.output,
});
}
}
store.dispatch({
type: 'problemconfig/addTestcases',
cases: pending,
});
}
return (<>
<li
className="bp4-tree-node"
onClick={auto}
>
<div className="bp4-tree-node-content bp4-tree-node-content-0">
<Icon icon="clean" />&nbsp;
<span className="bp4-tree-node-label">Auto detect</span>
</div>
</li>
<li
className="bp4-tree-node"
onClick={() => setOpen(true)}
>
<div className="bp4-tree-node-content bp4-tree-node-content-0">
<Icon icon="clean" />&nbsp;
<span className="bp4-tree-node-label">Add testcase</span>
</div>
</li>
<Dialog title="Add testcasse" icon="cog" isOpen={open} onClose={() => setOpen(false)}>
<DialogBody>
<ControlGroup fill={true} vertical={false}>
{/* TODO: autocomplete */}
<InputGroup
leftElement={<Icon icon="import" />}
onChange={(e) => setInput(e.target.value)}
placeholder="Input"
value={input || ''}
/>
<InputGroup
leftElement={<Icon icon="export" />}
onChange={(e) => setOutput(e.target.value)}
placeholder="Output"
value={output || ''}
/>
</ControlGroup>
</DialogBody>
<DialogFooter actions={<Button onClick={onConfirm} disabled={!valid} intent="primary" text="Save" />} />
</Dialog>
</>);
}
@@ -0,0 +1,130 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../reducer';
import { TestcaseGroup } from './Testcase';
interface SelectionManagerProps {
subtaskId: number;
subtaskIds: number[];
}
function collide(rect1: any, rect2: any): boolean {
if ('getBoundingClientRect' in rect1) rect1 = rect1.getBoundingClientRect();
if ('getBoundingClientRect' in rect2) rect2 = rect2.getBoundingClientRect();
const maxX = Math.max(rect1.x + rect1.width, rect2.x + rect2.width);
const maxY = Math.max(rect1.y + rect1.height, rect2.y + rect2.height);
const minX = Math.min(rect1.x, rect2.x);
const minY = Math.min(rect1.y, rect2.y);
return maxX - minX <= rect1.width + rect2.width && maxY - minY <= rect1.height + rect2.height;
}
export function SelectionManager(props: SelectionManagerProps) {
const { subtaskIds, subtaskId } = props;
const cases = useSelector((state: RootState) => (subtaskId === -1
? state.config.__cases || []
: state.config.subtasks.find((i) => i.id === subtaskId).cases || []));
// Don't need to trigger a re-render for this property change
const pos = React.useMemo(() => ({
x: 0, y: 0, endX: 0, endY: 0,
}), []);
const [start, setStart] = React.useState(0);
const [end, setEnd] = React.useState(0);
React.useEffect(() => {
setStart(0);
setEnd(0);
}, [JSON.stringify(cases)]);
const handleMouseDown = React.useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
pos.x = event.pageX;
pos.y = event.pageY;
// Check if clicking on a selected testcase
const selected = Array.from($('[data-selected="true"]'));
for (const el of selected) {
if (collide(el, { ...pos, width: 1, height: 1 })) return;
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
document.body.addEventListener('mousemove', handleMouseMove);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
document.body.addEventListener('mouseup', handleMouseUp);
$('body').css('cursor', 'crosshair')
.append('<div id="divSelectArea" style="position:absolute;background-color:#e073d4;"></div>');
$('#divSelectArea').css({
top: event.pageY,
left: event.pageX,
zIndex: 9999999,
}).fadeTo(12, 0.2);
}, [JSON.stringify(cases)]);
const handleMouseMove = React.useCallback((event) => {
pos.endX = event.pageX;
pos.endY = event.pageY;
$('#divSelectArea').css({
top: Math.min(event.pageY, pos.y),
left: Math.min(event.pageX, pos.x),
height: Math.abs(event.pageY - pos.y),
width: Math.abs(event.pageX - pos.x),
});
}, [JSON.stringify(cases)]);
const handleMouseUp = React.useCallback(() => {
document.body.removeEventListener('mousemove', handleMouseMove);
document.body.removeEventListener('mouseup', handleMouseUp);
const caseEntries = Array.from($(`[data-subtaskid="${subtaskId}"]`));
const selected = [];
for (let i = 0; i < caseEntries.length; i += 1) {
if (collide(caseEntries[i], $('#divSelectArea')[0])) {
selected.push(cases.indexOf(cases.find((c) => c.input === caseEntries[i].dataset.index)));
}
}
const sorted = selected.sort((a, b) => a - b);
$('#divSelectArea').remove();
$('body').css('cursor', 'default');
pos.x = pos.y = pos.endX = pos.endY = 0;
if (!sorted.length) return;
setStart(sorted[0]);
setEnd(sorted[selected.length - 1] + 1);
}, [JSON.stringify(cases)]);
return (
<>
{end > start && cases.slice(0, start).map((c, id) => (
<TestcaseGroup
subtaskId={subtaskId}
cases={[c]}
key={`${c.input}@${id}`}
selected={false}
subtaskIds={subtaskIds}
index={id}
onMouseDown={handleMouseDown}
onClick={() => {
setStart(id);
setEnd(id + 1);
}}
/>
))}
{start <= end && (
<TestcaseGroup
cases={cases.slice(start, end)}
subtaskId={subtaskId}
subtaskIds={subtaskIds}
onMouseDown={handleMouseDown}
selected={true}
index={start}
/>
)}
{end < cases.length && cases.slice(end).map((c, id) => (
<TestcaseGroup
subtaskId={subtaskId}
cases={[c]}
key={`${c.input}@${id}`}
subtaskIds={subtaskIds}
selected={false}
onMouseDown={handleMouseDown}
index={id + end}
onClick={() => {
setStart(id + end);
setEnd(id + end + 1);
}}
/>
))}
</>
);
}
@@ -0,0 +1,139 @@
import {
Button, ControlGroup,
Dialog, DialogBody, DialogFooter,
Icon, InputGroup,
} from '@blueprintjs/core';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../reducer';
interface SubtaskSettingsProps {
subtaskId: number;
time: string;
memory: string;
}
export function SubtaskSettings(props: SubtaskSettingsProps) {
const [open, setOpen] = React.useState(false);
const score = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).score);
const time = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).time);
const memory = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).memory);
const [ctime, setTime] = React.useState(time);
const [cmemory, setMemory] = React.useState(memory);
const [cscore, setScore] = React.useState(score);
const dispatcher = (func, key) => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
if (key === 'score') value = +value;
func(value);
};
const dispatch = useDispatch();
function onConfirm() {
dispatch({
type: 'problemconfig/updateSubtaskConfig',
id: props.subtaskId,
time: ctime,
memory: cmemory,
score: cscore,
});
setOpen(false);
}
return (<>
<Dialog title="Set limits" icon="cog" isOpen={open} onClose={() => setOpen(false)}>
<DialogBody>
<ControlGroup fill={true} vertical={false}>
<InputGroup
leftElement={<Icon icon="time" />}
onChange={dispatcher(setTime, 'time')}
placeholder={`Inherit (${props.time || '1s'})`}
value={ctime || ''}
/>
<InputGroup
leftElement={<Icon icon="comparison" />}
onChange={dispatcher(setMemory, 'memory')}
placeholder={`Inherit (${props.memory || '256m'})`}
value={cmemory || ''}
/>
<InputGroup
leftElement={<Icon icon="star" />}
onChange={dispatcher(setScore, 'score')}
placeholder="Score"
type="number"
value={cscore.toString()}
/>
</ControlGroup>
</DialogBody>
<DialogFooter actions={<Button onClick={onConfirm} intent="primary" text="Save" />} />
</Dialog>
<li className="bp4-tree-node" onClick={() => setOpen(true)}>
<div className="bp4-tree-node-content">
<span className="bp4-tree-node-caret-none bp4-icon-standard"></span>
<Icon icon="time" />
&nbsp;&nbsp;
<span className={`bp4-tree-node-label${!(time || props.time) ? ' text-gray' : ''}`}>{time || props.time || '1s'}</span>
<Icon icon="comparison" />
&nbsp;&nbsp;
<span className={`bp4-tree-node-label${!(memory || props.memory) ? ' text-gray' : ''}`}>{memory || props.memory || '256m'}</span>
<Icon icon="star" />
{' '}
<span className="bp4-tree-node-secondary-label">{score || 0}</span>
</div>
</li>
</>);
}
export function GlobalSettings() {
const time = useSelector((s: RootState) => s.config?.time);
const memory = useSelector((s: RootState) => s.config?.memory);
const [open, setOpen] = React.useState(false);
const [ctime, setTime] = React.useState(time);
const [cmemory, setMemory] = React.useState(memory);
React.useEffect(() => {
setTime(time);
}, [time]);
React.useEffect(() => {
setMemory(memory);
}, [memory]);
const dispatch = useDispatch();
function onConfirm() {
dispatch({
type: 'problemconfig/updateGlobalConfig',
time: ctime,
memory: cmemory,
});
setOpen(false);
}
return (<>
<Dialog title="Set limits" icon="cog" isOpen={open} onClose={() => setOpen(false)}>
<DialogBody>
<ControlGroup fill={true} vertical={false}>
<InputGroup
leftElement={<Icon icon="time" />}
onChange={(ev) => setTime(ev.currentTarget.value)}
placeholder="1s"
value={ctime || ''}
/>
<InputGroup
leftElement={<Icon icon="comparison" />}
onChange={(ev) => setMemory(ev.currentTarget.value)}
placeholder="256m"
value={cmemory || ''}
/>
</ControlGroup>
</DialogBody>
<DialogFooter actions={<Button onClick={onConfirm} intent="primary" text="Save" />} />
</Dialog>
<li className="bp4-tree-node" onClick={() => setOpen(true)}>
<div className="bp4-tree-node-content">
<Icon icon="time" />
&nbsp;&nbsp;
<span className={`bp4-tree-node-label${!time ? ' text-gray' : ''}`}>{time || '1s'}</span>
<Icon icon="comparison" />
{' '}
<span className={`bp4-tree-node-secondary-label${!memory ? ' text-gray' : ''}`}>{memory || '256m'}</span>
</div>
</li>
</>);
}
@@ -0,0 +1,77 @@
import { Menu, MenuItem, TreeNode } from '@blueprintjs/core';
import { ContextMenu2 } from '@blueprintjs/popover2';
import { TestCaseConfig } from 'hydrooj';
import { omit } from 'lodash';
import React from 'react';
import { useDrag } from 'react-dnd';
interface TestcaseNodeProps {
c: TestCaseConfig;
index: number;
time?: string;
memory?: string;
onClick?: () => void;
selected: boolean;
subtaskId: number;
subtaskIds: number[];
}
export function TestcaseNode(props: TestcaseNodeProps) {
const {
c, selected, onClick, subtaskIds, subtaskId,
} = props;
return (
<ContextMenu2
onContextMenu={onClick}
data-subtaskid={subtaskId}
data-index={c.input}
data-selected={selected}
content={
<Menu>
<MenuItem icon="drawer-left" text="Move to subtask" >
{subtaskIds.filter((i) => i !== subtaskId).map((i) => (
<MenuItem key={i} text={`Subtask ${i}`} />
))}
{subtaskIds.length <= 1 && (
<MenuItem icon="disable" disabled text="No target available" />
)}
</MenuItem>
</Menu>
}
>
<TreeNode
depth={0}
id={c.input}
isSelected={selected}
onClick={onClick}
icon="document"
label={<>&nbsp;{c.input} / {c.output}</>}
path={[0]}
/>
</ContextMenu2>
);
}
interface TestcaseGroupProps extends Omit<TestcaseNodeProps, 'c'> {
cases: TestCaseConfig[];
onMouseDown?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}
export function TestcaseGroup(props: TestcaseGroupProps) {
const {
cases, subtaskId, onClick, index,
} = props;
const [collected, drag] = useDrag(() => ({
type: 'cases',
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
canDrag: props.selected,
item: { cases, subtaskId },
}), [JSON.stringify(cases), subtaskId]);
return <div ref={drag} onClick={onClick} onMouseDown={props.onMouseDown} style={{ opacity: collected.isDragging ? 0.5 : 1 }}>
{cases.map((c, id) => (
<TestcaseNode c={c} key={`${c.input}@${index + id}`} {...omit(props, 'onClick')} index={index + id} />
))}
</div>;
}