Inline dataset row actions with modals
This commit is contained in:
@@ -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" });
|
||||||
|
if (selectedDatasetName === dataset.name || datasetName === dataset.name) {
|
||||||
setSelectedDatasetName("");
|
setSelectedDatasetName("");
|
||||||
setDatasetName("demo_dataset");
|
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);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<button type="button" className="datasetMiniMain" onClick={() => selectDataset(dataset)}>
|
||||||
<span>{dataset.name}</span>
|
<span>{dataset.name}</span>
|
||||||
<small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small>
|
<small>{datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair · {dataset.counts.images} images · {dataset.counts.masks} masks</small>
|
||||||
</button>
|
</button>
|
||||||
))}
|
<div className="datasetRowActions">
|
||||||
</div>
|
<button type="button" onClick={() => openPreprocessDataset(dataset)} title="图片预处理">
|
||||||
<label className="field compact">
|
<Wand2 size={15} />
|
||||||
<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>
|
||||||
|
<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>
|
||||||
</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">
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user