Inline dataset row actions with modals
This commit is contained in:
@@ -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" });
|
||||
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" : ""}`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user