mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
ui: problemconfig: remove old testcase config
This commit is contained in:
5 files changed
+8
-356
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.9.2",
|
"version": "4.9.3",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import { Card, NumericInput, Tag } from '@blueprintjs/core';
|
|
||||||
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
|
||||||
import type { SubtaskConfig } from 'hydrooj/src/interface';
|
|
||||||
import { isEqual, pick } from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { i18n } from 'vj/utils';
|
|
||||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
|
||||||
import { FormItem } from './BasicForm';
|
|
||||||
import { RootState } from './reducer';
|
|
||||||
import { CasesTable } from './TestCasesTable';
|
|
||||||
|
|
||||||
const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
|
|
||||||
|
|
||||||
export function SubtasksIds({ index }) {
|
|
||||||
const ids = useSelector((state: RootState) => state.config.subtasks.map((i) => (i ? i.id : undefined)).filter((i) => i !== undefined).join(','));
|
|
||||||
const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td style={{ textAlign: 'center' }}>if</td>
|
|
||||||
<td colSpan={4}>
|
|
||||||
<CustomSelectAutoComplete
|
|
||||||
data={ids.split(',').filter((i) => i.length)}
|
|
||||||
selectedKeys={subtaskIf?.map(String) || []}
|
|
||||||
onChange={(val) => dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
|
|
||||||
})}
|
|
||||||
multi
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SubtasksTable({ index }) {
|
|
||||||
const keys = ['id', 'type', 'score', 'time', 'memory'];
|
|
||||||
const subtask = useSelector((state: RootState) => pick(state.config.subtasks[index], keys), eq);
|
|
||||||
const defaultTime = useSelector((state: RootState) => state.config.time);
|
|
||||||
const defaultMemory = useSelector((state: RootState) => state.config.memory);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
|
||||||
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
|
|
||||||
if (value === 0) value = '';
|
|
||||||
if (value && suffix) value += suffix;
|
|
||||||
dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (!subtask || Object.keys(subtask).length === 0) {
|
|
||||||
return (
|
|
||||||
<Card style={{ padding: 0 }}>
|
|
||||||
<span>Subtasks #{index + 1} </span>
|
|
||||||
<p>{i18n('Failed to parse subtask.')}</p>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Card style={{ padding: 0 }}>
|
|
||||||
<span>Subtasks #{index + 1} </span>
|
|
||||||
<a onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' })}><span className="icon icon-add"></span></a>
|
|
||||||
<a
|
|
||||||
style={index === 0 ? { display: 'none' } : {}}
|
|
||||||
onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' })}
|
|
||||||
>
|
|
||||||
<span className="icon icon-delete"></span>
|
|
||||||
</a>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Score</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Memory</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{['id', 'score'].map((key) => (
|
|
||||||
<td key={key}>
|
|
||||||
<input
|
|
||||||
value={subtask[key] ?? ''}
|
|
||||||
onChange={dispatcher(key)}
|
|
||||||
type="number"
|
|
||||||
className="textbox"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td>
|
|
||||||
<select
|
|
||||||
value={subtask.type ?? 'min'}
|
|
||||||
onChange={dispatcher('type')}
|
|
||||||
className="select"
|
|
||||||
>
|
|
||||||
{['min', 'max', 'sum'].map((i) => (<option value={i} key={i}>{i}</option>))}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
|
||||||
value={subtask.time ? parseTimeMS(subtask.time, false).toString() : ''}
|
|
||||||
placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
|
|
||||||
onValueChange={dispatcher('time', 'ms')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
|
||||||
value={subtask.memory ? parseMemoryMB(subtask.memory, false).toString() : ''}
|
|
||||||
placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
|
|
||||||
onValueChange={dispatcher('memory', 'MB')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<SubtasksIds index={index} />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<CasesTable index={index} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GlobalTaskConfig() {
|
|
||||||
const time = useSelector((state: RootState) => state.config.time);
|
|
||||||
const memory = useSelector((state: RootState) => state.config.memory);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
|
||||||
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
|
|
||||||
if (value === 0) value = '';
|
|
||||||
if (value && suffix) value += suffix;
|
|
||||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key, value });
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormItem columns={6} label="Time">
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
|
||||||
value={time ? parseTimeMS(time, false).toString() : ''}
|
|
||||||
placeholder={parseTimeMS('1000ms').toString()}
|
|
||||||
onValueChange={dispatcher('time', 'ms')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem columns={6} label="Memory">
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
|
||||||
value={memory ? parseMemoryMB(memory, false).toString() : ''}
|
|
||||||
placeholder={parseMemoryMB('256m').toString()}
|
|
||||||
onValueChange={dispatcher('memory', 'MB')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TaskConfig() {
|
|
||||||
const len = useSelector((state: RootState) => state.config.subtasks?.length);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
return (
|
|
||||||
<FormItem columns={12} label="Task Settings">
|
|
||||||
<div className="row">
|
|
||||||
<GlobalTaskConfig />
|
|
||||||
<FormItem columns={12} label="TestCases" disableLabel>
|
|
||||||
{len > 0
|
|
||||||
? [...Array(len).keys()].map((i) => <SubtasksTable index={i} key={i} />)
|
|
||||||
: (
|
|
||||||
<>
|
|
||||||
<span>Subtasks # </span>
|
|
||||||
<a onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: 0, key: 'add' })}>
|
|
||||||
<span className="icon icon-add"></span>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { NumericInput, Tag } from '@blueprintjs/core';
|
|
||||||
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
|
||||||
import type { SubtaskConfig } from 'hydrooj/src/interface';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { i18n } from 'vj/utils';
|
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
|
||||||
import type { RootState } from './reducer/index';
|
|
||||||
|
|
||||||
const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
|
|
||||||
const eqArr = (a: any[], b: any[]) => isEqual(a, b);
|
|
||||||
|
|
||||||
export function TestCaseEntry({ index, subindex }) {
|
|
||||||
const testcase = useSelector((state: RootState) => state.config.subtasks[index].cases[subindex], eq);
|
|
||||||
const Files = useSelector((state: RootState) => state.testdata, eqArr);
|
|
||||||
const defaultTime = useSelector((state: RootState) => state.config.subtasks[index].time || state.config.time);
|
|
||||||
const defaultMemory = useSelector((state: RootState) => state.config.subtasks[index].memory || state.config.memory);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const dispatcher = (casesKey: string, valueSuffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
|
||||||
let value = typeof ev !== 'object' ? ev : ev.currentTarget.value;
|
|
||||||
if (value === 0) value = '';
|
|
||||||
if (valueSuffix && value) value += valueSuffix;
|
|
||||||
dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE',
|
|
||||||
id: index,
|
|
||||||
key: 'cases-edit',
|
|
||||||
casesId: subindex,
|
|
||||||
casesKey,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
if ((casesKey === 'input' && (value as string).endsWith('.in') && !testcase.output)
|
|
||||||
|| (casesKey === 'output' && (value as string).endsWith('.out') && !testcase.input)) {
|
|
||||||
const filename = (value as string).substring(0, (value as string).lastIndexOf('.'));
|
|
||||||
if (!testcase.output && (Files.map((f) => f.name).includes(`${filename}.out`) || Files.map((f) => f.name).includes(`${filename}.ans`))) {
|
|
||||||
dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE',
|
|
||||||
id: index,
|
|
||||||
key: 'cases-edit',
|
|
||||||
casesId: subindex,
|
|
||||||
casesKey: 'output',
|
|
||||||
value: Files.map((f) => f.name).includes(`${filename}.out`) ? `${filename}.out` : `${filename}.ans`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!testcase.input && Files.map((f) => f.name).includes(`${filename}.in`)) {
|
|
||||||
dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE',
|
|
||||||
id: index,
|
|
||||||
key: 'cases-edit',
|
|
||||||
casesId: subindex,
|
|
||||||
casesKey: 'input',
|
|
||||||
value: `${filename}.in`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const refs = {
|
|
||||||
input: useRef(),
|
|
||||||
output: useRef(),
|
|
||||||
};
|
|
||||||
for (const type of ['input', 'output']) {
|
|
||||||
useEffect(() => { // eslint-disable-line
|
|
||||||
refs[type].current?.setSelectedItems(testcase[type] ? [testcase[type]] : []);
|
|
||||||
}, [testcase[type]]);
|
|
||||||
}
|
|
||||||
if (!testcase || Object.keys(testcase).length === 0) {
|
|
||||||
return (
|
|
||||||
<tr><td colSpan={5}>{i18n('Failed to parse testcase.')}</td></tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
|
||||||
value={testcase.time ? parseTimeMS(testcase.time, false).toString() : ''}
|
|
||||||
placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
|
|
||||||
onValueChange={dispatcher('time', 'ms')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<NumericInput
|
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
|
||||||
value={testcase.memory ? parseMemoryMB(testcase.memory, false).toString() : ''}
|
|
||||||
placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
|
|
||||||
onValueChange={dispatcher('memory', 'MB')}
|
|
||||||
buttonPosition="none"
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
{['input', 'output'].map((t) => (
|
|
||||||
<td key={t}>
|
|
||||||
<FileSelectAutoComplete
|
|
||||||
ref={refs[t]}
|
|
||||||
width="100%"
|
|
||||||
data={Files}
|
|
||||||
selectedKeys={[testcase[t]]}
|
|
||||||
onChange={dispatcher(t)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td className="col--operation">
|
|
||||||
<a
|
|
||||||
onClick={() => dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
|
|
||||||
})}
|
|
||||||
><span className="icon icon-close"></span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CasesTable({ index }) {
|
|
||||||
const len = useSelector((state: RootState) => state.config.subtasks[index].cases?.length);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
return (
|
|
||||||
<table className="data-table">
|
|
||||||
<thead style={{ display: 'none' }}>
|
|
||||||
<tr>
|
|
||||||
<th>{i18n('Time')}</th>
|
|
||||||
<th>{i18n('Memory')}</th>
|
|
||||||
<th>{i18n('Input')}</th>
|
|
||||||
<th>{i18n('Output')}</th>
|
|
||||||
<th className="col--operation">
|
|
||||||
<a
|
|
||||||
onClick={() => dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE',
|
|
||||||
id: index,
|
|
||||||
key: 'cases-add',
|
|
||||||
value: { input: '', output: '' },
|
|
||||||
})}
|
|
||||||
><span className="icon icon-add" />
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr className="thead">
|
|
||||||
<td>{i18n('Time')}</td>
|
|
||||||
<td>{i18n('Memory')}</td>
|
|
||||||
<td>{i18n('Input')}</td>
|
|
||||||
<td>{i18n('Output')}</td>
|
|
||||||
<td className="col--operation">
|
|
||||||
<a
|
|
||||||
onClick={() => dispatch({
|
|
||||||
type: 'CONFIG_SUBTASK_UPDATE',
|
|
||||||
id: index,
|
|
||||||
key: 'cases-add',
|
|
||||||
value: { input: '', output: '' },
|
|
||||||
})}
|
|
||||||
><span className="icon icon-add" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{len > 0 && [...Array(len).keys()].map((i) => <TestCaseEntry index={index} subindex={i} key={i} />)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -64,15 +64,15 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
leftElement={<Icon icon="time" />}
|
leftElement={<Icon icon="time" />}
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
onChange={(ev) => setTime(`${ev.currentTarget.value}ms`)}
|
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
||||||
placeholder={`Inherit (${parseTimeMS(props.time, false) || '1000'})`}
|
placeholder={parseTimeMS(props.time, false).toString() || '1000'}
|
||||||
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
leftElement={<Icon icon="comparison" />}
|
leftElement={<Icon icon="comparison" />}
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
onChange={(ev) => setMemory(`${ev.currentTarget.value}MB`)}
|
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
||||||
placeholder={`Inherit (${parseMemoryMB(props.memory, false) || '256'})`}
|
placeholder={parseMemoryMB(props.memory, false).toString() || '256'}
|
||||||
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
@@ -167,14 +167,14 @@ export function GlobalSettings() {
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
leftElement={<Icon icon="time" />}
|
leftElement={<Icon icon="time" />}
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
onChange={(ev) => setTime(`${ev.currentTarget.value}ms`)}
|
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
||||||
placeholder="1000"
|
placeholder="1000"
|
||||||
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
leftElement={<Icon icon="comparison" />}
|
leftElement={<Icon icon="comparison" />}
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
onChange={(ev) => setMemory(`${ev.currentTarget.value}MB`)}
|
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
||||||
placeholder="256"
|
placeholder="256"
|
||||||
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.48.4",
|
"version": "4.48.5",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|||||||
Reference in new issue
Block a user