Inline dataset row actions with modals

This commit is contained in:
2026-07-01 13:15:33 +08:00
parent bb4c7ffa5e
commit 9fa64f355d
2 changed files with 172 additions and 58 deletions

View File

@@ -619,6 +619,8 @@ function App() {
const [busy, setBusy] = useState(false);
const [actionError, setActionError] = useState("");
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [copyModalOpen, setCopyModalOpen] = useState(false);
const [preprocessModalOpen, setPreprocessModalOpen] = useState(false);
const [datasetName, setDatasetName] = useState("demo_dataset");
const [datasetDescription, setDatasetDescription] = useState("");
const [copyDatasetName, setCopyDatasetName] = useState("");
@@ -1004,6 +1006,22 @@ function App() {
}
}
function selectDataset(dataset: UploadedDataset) {
setSelectedDatasetName(dataset.name);
setDatasetName(dataset.name);
}
function openCopyDataset(dataset: UploadedDataset) {
selectDataset(dataset);
setCopyDatasetName(`${dataset.name}_copy`);
setCopyModalOpen(true);
}
function openPreprocessDataset(dataset: UploadedDataset) {
selectDataset(dataset);
setPreprocessModalOpen(true);
}
async function copySelectedDataset() {
if (!selectedDataset || !copyDatasetName.trim()) return;
if (copyNameExists) {
@@ -1019,6 +1037,7 @@ function App() {
});
setSelectedDatasetName(copied.name);
setDatasetName(copied.name);
setCopyModalOpen(false);
await refresh();
} catch (err) {
setActionError(String(err));
@@ -1027,15 +1046,16 @@ function App() {
}
}
async function deleteSelectedDataset() {
if (!selectedDataset) return;
if (!window.confirm(`删除数据集 ${selectedDataset.name}?此操作会删除上传目录。`)) return;
async function deleteDataset(dataset: UploadedDataset) {
if (!window.confirm(`删除数据集 ${dataset.name}?此操作会删除上传目录。`)) return;
setBusy(true);
try {
setActionError("");
await api(`/api/datasets/${encodeURIComponent(selectedDataset.name)}`, { method: "DELETE" });
setSelectedDatasetName("");
setDatasetName("demo_dataset");
await api(`/api/datasets/${encodeURIComponent(dataset.name)}`, { method: "DELETE" });
if (selectedDatasetName === dataset.name || datasetName === dataset.name) {
setSelectedDatasetName("");
setDatasetName("demo_dataset");
}
await refresh();
} catch (err) {
setActionError(String(err));
@@ -1054,6 +1074,7 @@ function App() {
body: JSON.stringify(jobPayload(next, datasetParamsForTask(next)))
});
await inspectJob(job);
setPreprocessModalOpen(false);
navigate("training");
await refresh();
} catch (err) {
@@ -1430,49 +1451,26 @@ function App() {
</label>
<div className="datasetMiniList">
{datasets.map((dataset) => (
<button
<div
key={dataset.name}
type="button"
className={selectedDataset?.name === dataset.name ? "selected" : ""}
onClick={() => {
setSelectedDatasetName(dataset.name);
setDatasetName(dataset.name);
}}
className={`datasetMiniItem ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
>
<span>{dataset.name}</span>
<small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small>
</button>
))}
</div>
<label className="field compact">
<span></span>
<input value={copyDatasetName} onChange={(event) => setCopyDatasetName(event.target.value)} />
</label>
{copyNameExists && <small className="formWarn"></small>}
<div className="buttonRow">
<button className="primary secondary" disabled={busy || !selectedDataset || !copyDatasetName.trim() || copyNameExists} onClick={copySelectedDataset}><Copy size={17} /></button>
<button className="primary danger" disabled={busy || !selectedDataset} onClick={deleteSelectedDataset}><Trash2 size={17} /></button>
</div>
</div>
<div className="preprocessBox">
<div className="qualityHead">
<strong></strong>
<span>使</span>
</div>
<div className="preprocessControls">
<label><span></span><input type="number" min={1} value={preprocessWidth} onChange={(event) => setPreprocessWidth(Number(event.target.value) || 1)} /></label>
<label><span></span><input type="number" min={1} value={preprocessHeight} onChange={(event) => setPreprocessHeight(Number(event.target.value) || 1)} /></label>
<label><span></span><input value={preprocessPrefix} onChange={(event) => setPreprocessPrefix(event.target.value)} /></label>
<label><span></span><input value={preprocessSuffix} onChange={(event) => setPreprocessSuffix(event.target.value)} /></label>
<label><span></span><input type="number" min={0} max={1} step={0.05} value={preprocessAlpha} onChange={(event) => setPreprocessAlpha(Number(event.target.value) || 0)} /></label>
</div>
<div className="opGrid compactOps">
{manualDatasetOps.map((task) => (
<button key={task} type="button" disabled={busy || !selectedDataset?.absolute_layout} onClick={() => startDatasetPreprocessTask(task)}>
<Wand2 size={16} />
<span>{taskLabels[task]}</span>
</button>
<button type="button" className="datasetMiniMain" onClick={() => selectDataset(dataset)}>
<span>{dataset.name}</span>
<small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small>
</button>
<div className="datasetRowActions">
<button type="button" onClick={() => openPreprocessDataset(dataset)} title="图片预处理">
<Wand2 size={15} />
</button>
<button type="button" onClick={() => openCopyDataset(dataset)} title="复制数据集">
<Copy size={15} />
</button>
<button type="button" onClick={() => deleteDataset(dataset)} title="删除数据集">
<Trash2 size={15} />
</button>
</div>
</div>
))}
</div>
</div>
@@ -1553,6 +1551,67 @@ function App() {
</div>
)}
{copyModalOpen && selectedDataset && (
<div className="modalOverlay" data-page-section="datasets" role="dialog" aria-modal="true" aria-label="复制数据集">
<div className="datasetActionModal">
<div className="modalHead">
<div>
<p className="eyebrow">Copy</p>
<h2></h2>
<span className="pageHint">{selectedDataset.name}</span>
</div>
<button className="iconButton" type="button" onClick={() => setCopyModalOpen(false)} title="关闭">
<X size={18} />
</button>
</div>
<div className="datasetForm">
<label className="field compact">
<span></span>
<input value={copyDatasetName} onChange={(event) => setCopyDatasetName(event.target.value)} />
</label>
{copyNameExists && <small className="formWarn"></small>}
<div className="modalActions">
<button className="primary secondary" disabled={busy || !copyDatasetName.trim() || copyNameExists} onClick={copySelectedDataset}>
<Copy size={17} />
</button>
</div>
</div>
</div>
</div>
)}
{preprocessModalOpen && selectedDataset && (
<div className="modalOverlay" data-page-section="datasets" role="dialog" aria-modal="true" aria-label="图片预处理">
<div className="datasetActionModal wide">
<div className="modalHead">
<div>
<p className="eyebrow">Preprocess</p>
<h2></h2>
<span className="pageHint">{selectedDataset.name}</span>
</div>
<button className="iconButton" type="button" onClick={() => setPreprocessModalOpen(false)} title="关闭">
<X size={18} />
</button>
</div>
<div className="preprocessControls">
<label><span></span><input type="number" min={1} value={preprocessWidth} onChange={(event) => setPreprocessWidth(Number(event.target.value) || 1)} /></label>
<label><span></span><input type="number" min={1} value={preprocessHeight} onChange={(event) => setPreprocessHeight(Number(event.target.value) || 1)} /></label>
<label><span></span><input value={preprocessPrefix} onChange={(event) => setPreprocessPrefix(event.target.value)} /></label>
<label><span></span><input value={preprocessSuffix} onChange={(event) => setPreprocessSuffix(event.target.value)} /></label>
<label><span></span><input type="number" min={0} max={1} step={0.05} value={preprocessAlpha} onChange={(event) => setPreprocessAlpha(Number(event.target.value) || 0)} /></label>
</div>
<div className="opGrid preprocessOps">
{manualDatasetOps.map((task) => (
<button key={task} type="button" disabled={busy || !selectedDataset.absolute_layout} onClick={() => startDatasetPreprocessTask(task)}>
<Wand2 size={16} />
<span>{taskLabels[task]}</span>
</button>
))}
</div>
</div>
</div>
)}
<section className="grid two" id="inference" data-page-section="inference">
<div className="panel">
<div className="panelHead">

View File

@@ -747,14 +747,12 @@ textarea {
}
.datasetManager,
.preprocessBox,
.datasetDetailStack {
display: grid;
gap: 12px;
}
.datasetManager,
.preprocessBox {
.datasetManager {
padding-top: 14px;
border-top: 1px solid var(--line);
}
@@ -789,6 +787,21 @@ textarea {
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.55);
}
.datasetActionModal {
width: min(520px, 100%);
max-height: calc(100vh - 56px);
overflow: auto;
padding: 18px;
border: 1px solid rgba(157, 226, 111, 0.32);
border-radius: 8px;
background: #151815;
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.55);
}
.datasetActionModal.wide {
width: min(760px, 100%);
}
.modalHead {
display: flex;
align-items: center;
@@ -904,6 +917,11 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.opGrid.preprocessOps {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 12px;
}
.datasetMiniList {
display: grid;
gap: 7px;
@@ -911,35 +929,72 @@ textarea {
overflow: auto;
}
.datasetMiniList button {
.datasetMiniItem {
min-width: 0;
display: grid;
gap: 3px;
padding: 9px;
text-align: left;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 6px;
border: 1px solid var(--line);
border-radius: 6px;
background: #101310;
color: var(--ink);
}
.datasetMiniList button.selected {
.datasetMiniItem.selected {
border-color: var(--green);
background: rgba(157, 226, 111, 0.08);
}
.datasetMiniList span,
.datasetMiniList small {
.datasetMiniMain {
min-width: 0;
display: grid;
gap: 3px;
padding: 3px;
text-align: left;
background: transparent;
color: var(--ink);
}
.datasetMiniMain span,
.datasetMiniMain small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasetMiniList small {
.datasetMiniMain small {
color: var(--muted);
}
.datasetRowActions {
display: flex;
gap: 4px;
}
.datasetRowActions button {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 5px;
border: 1px solid var(--line);
background: #0b0d0b;
color: var(--muted);
}
.datasetRowActions button:hover {
color: var(--green);
border-color: rgba(157, 226, 111, 0.45);
}
.datasetRowActions button:last-child:hover {
color: var(--red);
border-color: rgba(240, 113, 103, 0.5);
}
.preprocessControls {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));