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 [busy, setBusy] = useState(false);
const [actionError, setActionError] = useState(""); const [actionError, setActionError] = useState("");
const [uploadModalOpen, setUploadModalOpen] = useState(false); const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [copyModalOpen, setCopyModalOpen] = useState(false);
const [preprocessModalOpen, setPreprocessModalOpen] = useState(false);
const [datasetName, setDatasetName] = useState("demo_dataset"); const [datasetName, setDatasetName] = useState("demo_dataset");
const [datasetDescription, setDatasetDescription] = useState(""); const [datasetDescription, setDatasetDescription] = useState("");
const [copyDatasetName, setCopyDatasetName] = 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() { async function copySelectedDataset() {
if (!selectedDataset || !copyDatasetName.trim()) return; if (!selectedDataset || !copyDatasetName.trim()) return;
if (copyNameExists) { if (copyNameExists) {
@@ -1019,6 +1037,7 @@ function App() {
}); });
setSelectedDatasetName(copied.name); setSelectedDatasetName(copied.name);
setDatasetName(copied.name); setDatasetName(copied.name);
setCopyModalOpen(false);
await refresh(); await refresh();
} catch (err) { } catch (err) {
setActionError(String(err)); setActionError(String(err));
@@ -1027,15 +1046,16 @@ function App() {
} }
} }
async function deleteSelectedDataset() { async function deleteDataset(dataset: UploadedDataset) {
if (!selectedDataset) return; if (!window.confirm(`删除数据集 ${dataset.name}?此操作会删除上传目录。`)) return;
if (!window.confirm(`删除数据集 ${selectedDataset.name}?此操作会删除上传目录。`)) return;
setBusy(true); setBusy(true);
try { try {
setActionError(""); setActionError("");
await api(`/api/datasets/${encodeURIComponent(selectedDataset.name)}`, { method: "DELETE" }); await api(`/api/datasets/${encodeURIComponent(dataset.name)}`, { method: "DELETE" });
setSelectedDatasetName(""); if (selectedDatasetName === dataset.name || datasetName === dataset.name) {
setDatasetName("demo_dataset"); setSelectedDatasetName("");
setDatasetName("demo_dataset");
}
await refresh(); await refresh();
} catch (err) { } catch (err) {
setActionError(String(err)); setActionError(String(err));
@@ -1054,6 +1074,7 @@ function App() {
body: JSON.stringify(jobPayload(next, datasetParamsForTask(next))) body: JSON.stringify(jobPayload(next, datasetParamsForTask(next)))
}); });
await inspectJob(job); await inspectJob(job);
setPreprocessModalOpen(false);
navigate("training"); navigate("training");
await refresh(); await refresh();
} catch (err) { } catch (err) {
@@ -1430,49 +1451,26 @@ function App() {
</label> </label>
<div className="datasetMiniList"> <div className="datasetMiniList">
{datasets.map((dataset) => ( {datasets.map((dataset) => (
<button <div
key={dataset.name} key={dataset.name}
type="button" className={`datasetMiniItem ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
className={selectedDataset?.name === dataset.name ? "selected" : ""}
onClick={() => {
setSelectedDatasetName(dataset.name);
setDatasetName(dataset.name);
}}
> >
<span>{dataset.name}</span> <button type="button" className="datasetMiniMain" onClick={() => selectDataset(dataset)}>
<small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small> <span>{dataset.name}</span>
</button> <small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small>
))} </button>
</div> <div className="datasetRowActions">
<label className="field compact"> <button type="button" onClick={() => openPreprocessDataset(dataset)} title="图片预处理">
<span></span> <Wand2 size={15} />
<input value={copyDatasetName} onChange={(event) => setCopyDatasetName(event.target.value)} /> </button>
</label> <button type="button" onClick={() => openCopyDataset(dataset)} title="复制数据集">
{copyNameExists && <small className="formWarn"></small>} <Copy size={15} />
<div className="buttonRow"> </button>
<button className="primary secondary" disabled={busy || !selectedDataset || !copyDatasetName.trim() || copyNameExists} onClick={copySelectedDataset}><Copy size={17} /></button> <button type="button" onClick={() => deleteDataset(dataset)} title="删除数据集">
<button className="primary danger" disabled={busy || !selectedDataset} onClick={deleteSelectedDataset}><Trash2 size={17} /></button> <Trash2 size={15} />
</div> </button>
</div> </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>
))} ))}
</div> </div>
</div> </div>
@@ -1553,6 +1551,67 @@ function App() {
</div> </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"> <section className="grid two" id="inference" data-page-section="inference">
<div className="panel"> <div className="panel">
<div className="panelHead"> <div className="panelHead">

View File

@@ -747,14 +747,12 @@ textarea {
} }
.datasetManager, .datasetManager,
.preprocessBox,
.datasetDetailStack { .datasetDetailStack {
display: grid; display: grid;
gap: 12px; gap: 12px;
} }
.datasetManager, .datasetManager {
.preprocessBox {
padding-top: 14px; padding-top: 14px;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
@@ -789,6 +787,21 @@ textarea {
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.55); 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 { .modalHead {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -904,6 +917,11 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.opGrid.preprocessOps {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 12px;
}
.datasetMiniList { .datasetMiniList {
display: grid; display: grid;
gap: 7px; gap: 7px;
@@ -911,35 +929,72 @@ textarea {
overflow: auto; overflow: auto;
} }
.datasetMiniList button { .datasetMiniItem {
min-width: 0; min-width: 0;
display: grid; display: grid;
gap: 3px; grid-template-columns: minmax(0, 1fr) auto;
padding: 9px; align-items: center;
text-align: left; gap: 8px;
padding: 6px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 6px; border-radius: 6px;
background: #101310; background: #101310;
color: var(--ink); color: var(--ink);
} }
.datasetMiniList button.selected { .datasetMiniItem.selected {
border-color: var(--green); border-color: var(--green);
background: rgba(157, 226, 111, 0.08); background: rgba(157, 226, 111, 0.08);
} }
.datasetMiniList span, .datasetMiniMain {
.datasetMiniList small { min-width: 0;
display: grid;
gap: 3px;
padding: 3px;
text-align: left;
background: transparent;
color: var(--ink);
}
.datasetMiniMain span,
.datasetMiniMain small {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.datasetMiniList small { .datasetMiniMain small {
color: var(--muted); 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 { .preprocessControls {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));