feat(collect): support batch add in NE picker

Allow selecting multiple eligible NEs and adding them into the selected list in one action, while keeping single-item add for precise control.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-28 10:47:26 +08:00
parent f1ed4fe753
commit 52f1b63b18
3 changed files with 82 additions and 6 deletions

View file

@ -56,6 +56,7 @@ const en = {
filterKeyword: "Filter", filterKeyword: "Filter",
filterKeywordPh: "Name / IP / vendor / device type", filterKeywordPh: "Name / IP / vendor / device type",
add: "Add", add: "Add",
addBatch: "Add selected ({{count}})",
added: "Added", added: "Added",
create: "Create job", create: "Create job",
creating: "Creating…", creating: "Creating…",

View file

@ -56,6 +56,7 @@ const zh = {
filterKeyword: "筛选", filterKeyword: "筛选",
filterKeywordPh: "名称 / IP / 厂商 / 设备类型", filterKeywordPh: "名称 / IP / 厂商 / 设备类型",
add: "添加", add: "添加",
addBatch: "批量添加({{count}})",
added: "已添加", added: "已添加",
create: "创建任务", create: "创建任务",
creating: "创建中…", creating: "创建中…",

View file

@ -31,6 +31,7 @@ export function CollectPage() {
const [commands, setCommands] = useState(""); const [commands, setCommands] = useState("");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [selectedMap, setSelectedMap] = useState<Record<string, EligibleNeItem>>({}); const [selectedMap, setSelectedMap] = useState<Record<string, EligibleNeItem>>({});
const [pickSelectedIds, setPickSelectedIds] = useState<string[]>([]);
const [neKeyword, setNeKeyword] = useState(""); const [neKeyword, setNeKeyword] = useState("");
const [nePage, setNePage] = useState(1); const [nePage, setNePage] = useState(1);
const [jobPage, setJobPage] = useState(1); const [jobPage, setJobPage] = useState(1);
@ -150,6 +151,17 @@ export function CollectPage() {
setSelectedMap((prev) => (prev[row.id] ? prev : { ...prev, [row.id]: row })); setSelectedMap((prev) => (prev[row.id] ? prev : { ...prev, [row.id]: row }));
}; };
const addBatchNe = (rows: EligibleNeItem[]) => {
if (rows.length === 0) return;
setSelectedMap((prev) => {
const next = { ...prev };
for (const row of rows) {
if (!next[row.id]) next[row.id] = row;
}
return next;
});
};
const removeNe = (id: string) => { const removeNe = (id: string) => {
setSelectedMap((prev) => { setSelectedMap((prev) => {
const next = { ...prev }; const next = { ...prev };
@ -161,6 +173,9 @@ export function CollectPage() {
const clearSelected = () => setSelectedMap({}); const clearSelected = () => setSelectedMap({});
const eligibleItems = eligibleQuery.data?.items ?? []; const eligibleItems = eligibleQuery.data?.items ?? [];
const selectablePickItems = eligibleItems.filter((row) => !selectedMap[row.id]);
const allPickSelected = selectablePickItems.length > 0 && selectablePickItems.every((row) => pickSelectedIds.includes(row.id));
const batchPickCount = pickSelectedIds.length;
const neTotal = eligibleQuery.data?.total ?? 0; const neTotal = eligibleQuery.data?.total ?? 0;
const nePages = pageCount(neTotal, ELIGIBLE_PAGE_SIZE); const nePages = pageCount(neTotal, ELIGIBLE_PAGE_SIZE);
@ -246,9 +261,23 @@ export function CollectPage() {
<h3>{t("collect.create.pickTitle")}</h3> <h3>{t("collect.create.pickTitle")}</h3>
<p className="panel__hint">{t("collect.create.pickHint")}</p> <p className="panel__hint">{t("collect.create.pickHint")}</p>
</div> </div>
<button type="button" onClick={() => eligibleQuery.refetch()} disabled={eligibleQuery.isFetching}> <div className="table-actions">
{eligibleQuery.isFetching ? t("common.refreshing") : t("common.refresh")} <button
</button> type="button"
className="link-btn"
disabled={batchPickCount === 0}
onClick={() => {
const rows = eligibleItems.filter((x) => pickSelectedIds.includes(x.id) && !selectedMap[x.id]);
addBatchNe(rows);
setPickSelectedIds([]);
}}
>
{t("collect.create.addBatch", { count: batchPickCount })}
</button>
<button type="button" onClick={() => eligibleQuery.refetch()} disabled={eligibleQuery.isFetching}>
{eligibleQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
</button>
</div>
</div> </div>
<div className="collect-pick-filters"> <div className="collect-pick-filters">
<label className="collect-runs-filter"> <label className="collect-runs-filter">
@ -260,6 +289,7 @@ export function CollectPage() {
onChange={(e) => { onChange={(e) => {
setNeKeyword(e.target.value); setNeKeyword(e.target.value);
setNePage(1); setNePage(1);
setPickSelectedIds([]);
}} }}
/> />
</label> </label>
@ -271,6 +301,20 @@ export function CollectPage() {
<table> <table>
<thead> <thead>
<tr> <tr>
<th>
<input
type="checkbox"
checked={allPickSelected}
onChange={(e) => {
if (e.target.checked) {
setPickSelectedIds(selectablePickItems.map((row) => row.id));
return;
}
setPickSelectedIds([]);
}}
aria-label="pick all"
/>
</th>
<th>{t("managedNe.col.name")}</th> <th>{t("managedNe.col.name")}</th>
<th>{t("managedNe.col.vendor")}</th> <th>{t("managedNe.col.vendor")}</th>
<th>{t("managedNe.col.ip")}</th> <th>{t("managedNe.col.ip")}</th>
@ -283,6 +327,19 @@ export function CollectPage() {
const picked = Boolean(selectedMap[row.id]); const picked = Boolean(selectedMap[row.id]);
return ( return (
<tr key={row.id}> <tr key={row.id}>
<td>
<input
type="checkbox"
checked={pickSelectedIds.includes(row.id)}
disabled={picked}
onChange={(e) => {
if (picked) return;
setPickSelectedIds((prev) =>
e.target.checked ? [...new Set([...prev, row.id])] : prev.filter((id) => id !== row.id),
);
}}
/>
</td>
<td>{row.name || row.ip_address}</td> <td>{row.name || row.ip_address}</td>
<td>{row.vendor}</td> <td>{row.vendor}</td>
<td>{row.ip_address}</td> <td>{row.ip_address}</td>
@ -294,7 +351,10 @@ export function CollectPage() {
type="button" type="button"
className="link-btn" className="link-btn"
disabled={picked} disabled={picked}
onClick={() => addNe(row)} onClick={() => {
addNe(row);
setPickSelectedIds((prev) => prev.filter((id) => id !== row.id));
}}
> >
{picked ? t("collect.create.added") : t("collect.create.add")} {picked ? t("collect.create.added") : t("collect.create.add")}
</button> </button>
@ -311,10 +371,24 @@ export function CollectPage() {
{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })} {t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}
</div> </div>
<div className="pager__controls"> <div className="pager__controls">
<button className="pager__btn" disabled={nePage <= 1} onClick={() => setNePage(nePage - 1)}> <button
className="pager__btn"
disabled={nePage <= 1}
onClick={() => {
setNePage(nePage - 1);
setPickSelectedIds([]);
}}
>
{t("common.prevPage")} {t("common.prevPage")}
</button> </button>
<button className="pager__btn" disabled={nePage >= nePages} onClick={() => setNePage(nePage + 1)}> <button
className="pager__btn"
disabled={nePage >= nePages}
onClick={() => {
setNePage(nePage + 1);
setPickSelectedIds([]);
}}
>
{t("common.nextPage")} {t("common.nextPage")}
</button> </button>
</div> </div>