mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Add collect dashboard KPIs and create-job modal.
Align collection tasks with LLDP/config-sync chrome, and refresh active-task overview when jobs change. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0100fb8430
commit
6642f4b8b5
11 changed files with 502 additions and 249 deletions
|
|
@ -28,6 +28,7 @@ export const queryKeys = {
|
|||
collectionEligibleNe: (page: number, keyword: string) => ["collectionEligibleNe", page, keyword] as const,
|
||||
neCollectionsAll: ["neCollections"] as const,
|
||||
neCollections: (page: number) => ["neCollections", page] as const,
|
||||
neCollectionDashboard: ["neCollectionDashboard"] as const,
|
||||
neCollectionDetail: (jobId: string) => ["neCollection", jobId] as const,
|
||||
neCollectionRunsAll: ["neCollectionRuns"] as const,
|
||||
neCollectionRuns: (jobId: string, page: number, status: string, keyword: string) =>
|
||||
|
|
|
|||
|
|
@ -604,8 +604,8 @@ const en = {
|
|||
added: "Added",
|
||||
create: "Create job",
|
||||
creating: "Creating…",
|
||||
expand: "Expand",
|
||||
collapse: "Collapse",
|
||||
expand: "New",
|
||||
collapse: "Close",
|
||||
meta: "{{ne}} NE(s) selected · {{cmd}} command(s)",
|
||||
},
|
||||
eligible: {
|
||||
|
|
@ -659,6 +659,13 @@ const en = {
|
|||
autoRefresh: "Auto-refresh every 2s while jobs are running",
|
||||
runsInProgress: "Some devices are still being collected…",
|
||||
},
|
||||
kpi: {
|
||||
jobs: "Jobs",
|
||||
running: "In progress",
|
||||
idle: "Idle",
|
||||
last: "Last job",
|
||||
active: "Active jobs",
|
||||
},
|
||||
runs: {
|
||||
status: "Status",
|
||||
message: "Message",
|
||||
|
|
|
|||
|
|
@ -600,8 +600,8 @@ const zh = {
|
|||
added: "已添加",
|
||||
create: "创建任务",
|
||||
creating: "创建中…",
|
||||
expand: "展开",
|
||||
collapse: "收起",
|
||||
expand: "新建",
|
||||
collapse: "关闭",
|
||||
meta: "已选 {{ne}} 台网元 · {{cmd}} 条命令",
|
||||
},
|
||||
eligible: {
|
||||
|
|
@ -655,6 +655,13 @@ const zh = {
|
|||
autoRefresh: "任务进行中,每 2 秒自动刷新",
|
||||
runsInProgress: "部分网元仍在采集中,请稍候…",
|
||||
},
|
||||
kpi: {
|
||||
jobs: "任务总数",
|
||||
running: "进行中",
|
||||
idle: "空闲",
|
||||
last: "最近任务",
|
||||
active: "活跃任务",
|
||||
},
|
||||
runs: {
|
||||
status: "状态",
|
||||
message: "说明",
|
||||
|
|
|
|||
|
|
@ -2525,6 +2525,16 @@ pre {
|
|||
max-height: 88px;
|
||||
}
|
||||
|
||||
.collect-create-modal .collect-selected-block,
|
||||
.collect-create-modal .collect-pick-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.collect-create-modal .panel__toolbar {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.collect-runs-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import {
|
||||
createNeCollection,
|
||||
deleteCollectionJob,
|
||||
fetchCollectionDashboard,
|
||||
fetchCollectionJob,
|
||||
fetchCollectionRuns,
|
||||
fetchEligibleNe,
|
||||
|
|
@ -58,6 +59,7 @@ export function CollectPage() {
|
|||
queryFn: () =>
|
||||
fetchEligibleNe({ page: nePage, pageSize: ELIGIBLE_PAGE_SIZE, keyword: neKeyword }),
|
||||
staleTime: 5000,
|
||||
enabled: createOpen,
|
||||
});
|
||||
|
||||
const jobsQuery = useQuery({
|
||||
|
|
@ -66,7 +68,20 @@ export function CollectPage() {
|
|||
staleTime: 1000,
|
||||
refetchInterval: (q) => {
|
||||
const items = q.state.data?.items ?? [];
|
||||
return items.some((j) => j.status === "running") ? POLL_MS : false;
|
||||
return items.some((j) => j.status === "running" || j.status === "pending") ? POLL_MS : false;
|
||||
},
|
||||
});
|
||||
|
||||
const dashQuery = useQuery({
|
||||
queryKey: queryKeys.neCollectionDashboard,
|
||||
queryFn: fetchCollectionDashboard,
|
||||
staleTime: 1000,
|
||||
refetchInterval: (q) => {
|
||||
const running = q.state.data?.running_job;
|
||||
return running &&
|
||||
(running.status === "running" || running.status === "pending" || running.status === "paused")
|
||||
? POLL_MS
|
||||
: false;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -79,15 +94,24 @@ export function CollectPage() {
|
|||
});
|
||||
|
||||
const jobs = jobsQuery.data?.items ?? [];
|
||||
const jobActive = jobs.some((j) => j.status === "running");
|
||||
const dash = dashQuery.data;
|
||||
const running = dash?.running_job;
|
||||
const last = dash?.last_job;
|
||||
const jobActive = Boolean(
|
||||
running && (running.status === "running" || running.status === "pending" || running.status === "paused"),
|
||||
);
|
||||
const expandedJobRunning =
|
||||
Boolean(expandedJobId) &&
|
||||
(jobs.find((j) => j.id === expandedJobId)?.status === "running" || detailQuery.data?.job.status === "running");
|
||||
const autoPolling = jobActive || expandedJobRunning;
|
||||
|
||||
const invalidateOpsTasks = () => queryClient.invalidateQueries({ queryKey: ["opsTasks"] });
|
||||
|
||||
const refreshAll = async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDashboard }),
|
||||
invalidateOpsTasks(),
|
||||
expandedJobId
|
||||
? queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(expandedJobId) })
|
||||
: Promise.resolve(),
|
||||
|
|
@ -99,6 +123,8 @@ export function CollectPage() {
|
|||
|
||||
const invalidateJobs = async (jobId?: string) => {
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDashboard });
|
||||
await invalidateOpsTasks();
|
||||
if (jobId) {
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(jobId) });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionRunsAll });
|
||||
|
|
@ -163,8 +189,7 @@ export function CollectPage() {
|
|||
showOk(t("collect.created", { id: job.id }));
|
||||
setCreateOpen(false);
|
||||
setExpandedJobId(job.id);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(job.id) });
|
||||
await invalidateJobs(job.id);
|
||||
},
|
||||
onError: (err) => showError(String(err)),
|
||||
});
|
||||
|
|
@ -225,241 +250,6 @@ export function CollectPage() {
|
|||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<section className={`panel collect-create-panel${createOpen ? "" : " collect-create-panel--collapsed"}`}>
|
||||
<div className="panel__toolbar collect-create-panel__head">
|
||||
<div>
|
||||
<h2>{t("collect.create.title")}</h2>
|
||||
<p className="panel__hint">
|
||||
{createOpen
|
||||
? t("collect.create.hint")
|
||||
: t("collect.create.meta", { ne: selectedIds.length, cmd: commandLines })}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="link-btn" onClick={() => setCreateOpen((open) => !open)}>
|
||||
{createOpen ? t("collect.create.collapse") : t("collect.create.expand")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{createOpen ? (
|
||||
<>
|
||||
<div className="form-grid form-grid--single">
|
||||
<label className="form-grid__full">
|
||||
{t("collect.form.jobTitle")}
|
||||
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder={t("collect.form.jobTitlePh")} />
|
||||
</label>
|
||||
<label className="form-grid__full">
|
||||
{t("collect.form.commands")}
|
||||
<textarea
|
||||
className="collect-commands"
|
||||
rows={8}
|
||||
value={commands}
|
||||
onChange={(e) => setCommands(e.target.value)}
|
||||
placeholder={t("collect.form.commandsPh")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p className="panel__hint">{t("collect.form.commandsHint")}</p>
|
||||
|
||||
<div className="collect-selected-block">
|
||||
<div className="collect-selected-block__head">
|
||||
<h3>{t("collect.create.selectedTitle")}</h3>
|
||||
<span className="collect-meta">{t("collect.create.selectedCount", { count: selectedList.length })}</span>
|
||||
{selectedList.length > 0 ? (
|
||||
<button type="button" className="link-btn" onClick={clearSelected}>
|
||||
{t("collect.create.clearSelected")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedList.length === 0 ? (
|
||||
<p className="panel__hint">{t("collect.create.selectedEmpty")}</p>
|
||||
) : (
|
||||
<div className="collect-selected-list">
|
||||
{selectedList.map((row) => {
|
||||
const key = eligibleKey(row);
|
||||
const src = String(row.source || "managed").toLowerCase();
|
||||
return (
|
||||
<div key={key} className="collect-selected-chip">
|
||||
<span className="collect-selected-chip__main">
|
||||
<strong>{row.name || row.ip_address}</strong>
|
||||
<span className="collect-selected-chip__meta">
|
||||
{src}
|
||||
{row.ip_address ? ` · ${row.ip_address}` : ""}
|
||||
{row.vendor ? ` · ${row.vendor}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" className="link-btn" onClick={() => removeNe(key)}>
|
||||
{t("collect.create.remove")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="collect-pick-block">
|
||||
<div className="panel__toolbar">
|
||||
<div>
|
||||
<h3>{t("collect.create.pickTitle")}</h3>
|
||||
<p className="panel__hint">{t("collect.create.pickHint")}</p>
|
||||
</div>
|
||||
<div className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
disabled={batchPickCount === 0}
|
||||
onClick={() => {
|
||||
const rows = eligibleItems.filter(
|
||||
(x) => pickSelectedIds.includes(eligibleKey(x)) && !selectedMap[eligibleKey(x)],
|
||||
);
|
||||
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 className="collect-pick-filters">
|
||||
<label className="collect-runs-filter">
|
||||
{t("collect.create.filterKeyword")}
|
||||
<input
|
||||
type="search"
|
||||
value={neKeyword}
|
||||
placeholder={t("collect.create.filterKeywordPh")}
|
||||
onChange={(e) => {
|
||||
setNeKeyword(e.target.value);
|
||||
setNePage(1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{eligibleQuery.isLoading ? <p>{t("common.refreshing")}</p> : null}
|
||||
{!eligibleQuery.isLoading && eligibleItems.length === 0 ? (
|
||||
<p>{t("collect.create.pickEmpty")}</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allPickSelected}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setPickSelectedIds(selectablePickItems.map((row) => eligibleKey(row)));
|
||||
return;
|
||||
}
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
aria-label="pick all"
|
||||
/>
|
||||
</th>
|
||||
<th>{t("managedNe.col.source")}</th>
|
||||
<th>{t("managedNe.col.name")}</th>
|
||||
<th>{t("managedNe.col.vendor")}</th>
|
||||
<th>{t("managedNe.col.ip")}</th>
|
||||
<th>{t("managedNe.col.connect")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{eligibleItems.map((row) => {
|
||||
const key = eligibleKey(row);
|
||||
const picked = Boolean(selectedMap[key]);
|
||||
const src = String(row.source || "managed").toLowerCase();
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pickSelectedIds.includes(key)}
|
||||
disabled={picked}
|
||||
onChange={(e) => {
|
||||
if (picked) return;
|
||||
setPickSelectedIds((prev) =>
|
||||
e.target.checked ? [...new Set([...prev, key])] : prev.filter((id) => id !== key),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span className="table-tag">{src}</span>
|
||||
</td>
|
||||
<td>{row.name || row.ip_address}</td>
|
||||
<td>{row.vendor}</td>
|
||||
<td>{row.ip_address}</td>
|
||||
<td>
|
||||
<span className="conn-pill conn-pill--up">{row.connect_status}</span>
|
||||
</td>
|
||||
<td className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
disabled={picked}
|
||||
onClick={() => {
|
||||
addNe(row);
|
||||
setPickSelectedIds((prev) => prev.filter((id) => id !== key));
|
||||
}}
|
||||
>
|
||||
{picked ? t("collect.create.added") : t("collect.create.add")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{neTotal > 0 ? (
|
||||
<div className="pager">
|
||||
<div className="pager__meta">
|
||||
{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}
|
||||
</div>
|
||||
<div className="pager__controls">
|
||||
<button
|
||||
className="pager__btn"
|
||||
disabled={nePage <= 1}
|
||||
onClick={() => {
|
||||
setNePage(nePage - 1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<button
|
||||
className="pager__btn"
|
||||
disabled={nePage >= nePages}
|
||||
onClick={() => {
|
||||
setNePage(nePage + 1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="panel__actions">
|
||||
<span className="collect-meta">{t("collect.create.meta", { ne: selectedIds.length, cmd: commandLines })}</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={selectedIds.length === 0 || commandLines === 0 || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
{createMutation.isPending ? t("collect.create.creating") : t("collect.create.create")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<div>
|
||||
|
|
@ -467,14 +257,58 @@ export function CollectPage() {
|
|||
{autoPolling ? (
|
||||
<p className="panel__hint panel__hint--live">
|
||||
{t("collect.jobs.autoRefresh")}
|
||||
{jobsQuery.isFetching ? ` · ${t("common.refreshing")}` : ""}
|
||||
{jobsQuery.isFetching || dashQuery.isFetching ? ` · ${t("common.refreshing")}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button type="button" onClick={refreshAll} disabled={jobsQuery.isFetching}>
|
||||
{jobsQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||
</button>
|
||||
<div className="btn-row">
|
||||
<button type="button" onClick={refreshAll} disabled={jobsQuery.isFetching || dashQuery.isFetching}>
|
||||
{jobsQuery.isFetching || dashQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||
</button>
|
||||
<button type="button" className="btn-primary" onClick={() => setCreateOpen(true)}>
|
||||
{t("collect.create.expand")}
|
||||
</button>
|
||||
{running?.status === "running" || running?.status === "pending" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pauseMutation.mutate(running.id)}
|
||||
disabled={pauseMutation.isPending}
|
||||
>
|
||||
{t("collect.jobs.pause")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-list" style={{ marginBottom: 16 }}>
|
||||
<div className="pt-list-kpis">
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("collect.kpi.jobs")}</div>
|
||||
<div className="pt-list-kpi__value">{dash?.job_count ?? "—"}</div>
|
||||
</div>
|
||||
<div className={`pt-list-kpi${running ? " pt-list-kpi--live" : ""}`}>
|
||||
<div className="pt-list-kpi__label">{t("collect.kpi.running")}</div>
|
||||
<div className="pt-list-kpi__value" style={{ fontSize: running ? 15 : 22 }}>
|
||||
{running
|
||||
? `${running.status} · ${running.success_count}/${running.ne_count}`
|
||||
: t("collect.kpi.idle")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("collect.kpi.active")}</div>
|
||||
<div className="pt-list-kpi__value">{dash?.active_count ?? "—"}</div>
|
||||
</div>
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("collect.kpi.last")}</div>
|
||||
<div className="pt-list-kpi__value" style={{ fontSize: 15 }}>
|
||||
{last
|
||||
? `${last.title} · ${last.status} · ok ${last.success_count} / fail ${last.fail_count}`
|
||||
: t("common.empty")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-list">
|
||||
{!(jobsQuery.data?.items ?? []).length && !jobsQuery.isLoading ? (
|
||||
<div className="pt-list-empty">
|
||||
|
|
@ -533,6 +367,273 @@ export function CollectPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{createOpen ? (
|
||||
<div className="modal-backdrop" role="presentation" onClick={() => setCreateOpen(false)}>
|
||||
<div
|
||||
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("collect.create.title")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="ops-detail-modal__head">
|
||||
<div className="ops-detail-modal__title">
|
||||
<h3>{t("collect.create.title")}</h3>
|
||||
<p className="muted">{t("collect.create.hint")}</p>
|
||||
</div>
|
||||
<div className="btn-row ops-detail-modal__actions">
|
||||
<button type="button" onClick={() => setCreateOpen(false)}>
|
||||
{t("networkConfigs.close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-detail-modal__scroll ops-detail-modal__scroll--pad collect-create-modal">
|
||||
<div className="form-grid form-grid--single">
|
||||
<label className="form-grid__full">
|
||||
{t("collect.form.jobTitle")}
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t("collect.form.jobTitlePh")}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-grid__full">
|
||||
{t("collect.form.commands")}
|
||||
<textarea
|
||||
className="collect-commands"
|
||||
rows={6}
|
||||
value={commands}
|
||||
onChange={(e) => setCommands(e.target.value)}
|
||||
placeholder={t("collect.form.commandsPh")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p className="panel__hint">{t("collect.form.commandsHint")}</p>
|
||||
|
||||
<div className="collect-selected-block">
|
||||
<div className="collect-selected-block__head">
|
||||
<h3>{t("collect.create.selectedTitle")}</h3>
|
||||
<span className="collect-meta">
|
||||
{t("collect.create.selectedCount", { count: selectedList.length })}
|
||||
</span>
|
||||
{selectedList.length > 0 ? (
|
||||
<button type="button" className="link-btn" onClick={clearSelected}>
|
||||
{t("collect.create.clearSelected")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedList.length === 0 ? (
|
||||
<p className="panel__hint">{t("collect.create.selectedEmpty")}</p>
|
||||
) : (
|
||||
<div className="collect-selected-list">
|
||||
{selectedList.map((row) => {
|
||||
const key = eligibleKey(row);
|
||||
const src = String(row.source || "managed").toLowerCase();
|
||||
return (
|
||||
<div key={key} className="collect-selected-chip">
|
||||
<span className="collect-selected-chip__main">
|
||||
<strong>{row.name || row.ip_address}</strong>
|
||||
<span className="collect-selected-chip__meta">
|
||||
{src}
|
||||
{row.ip_address ? ` · ${row.ip_address}` : ""}
|
||||
{row.vendor ? ` · ${row.vendor}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" className="link-btn" onClick={() => removeNe(key)}>
|
||||
{t("collect.create.remove")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="collect-pick-block">
|
||||
<div className="panel__toolbar">
|
||||
<div>
|
||||
<h3>{t("collect.create.pickTitle")}</h3>
|
||||
<p className="panel__hint">{t("collect.create.pickHint")}</p>
|
||||
</div>
|
||||
<div className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
disabled={batchPickCount === 0}
|
||||
onClick={() => {
|
||||
const rows = eligibleItems.filter(
|
||||
(x) => pickSelectedIds.includes(eligibleKey(x)) && !selectedMap[eligibleKey(x)],
|
||||
);
|
||||
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 className="ops-detail-modal__toolbar filter-inline">
|
||||
<input
|
||||
type="search"
|
||||
value={neKeyword}
|
||||
placeholder={t("collect.create.filterKeywordPh")}
|
||||
onChange={(e) => {
|
||||
setNeKeyword(e.target.value);
|
||||
setNePage(1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{eligibleQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allPickSelected}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setPickSelectedIds(selectablePickItems.map((row) => eligibleKey(row)));
|
||||
return;
|
||||
}
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
aria-label="pick all"
|
||||
/>
|
||||
</th>
|
||||
<th>{t("managedNe.col.source")}</th>
|
||||
<th>{t("managedNe.col.name")}</th>
|
||||
<th>{t("managedNe.col.vendor")}</th>
|
||||
<th>{t("managedNe.col.ip")}</th>
|
||||
<th>{t("managedNe.col.connect")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{eligibleItems.map((row) => {
|
||||
const key = eligibleKey(row);
|
||||
const picked = Boolean(selectedMap[key]);
|
||||
const src = String(row.source || "managed").toLowerCase();
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pickSelectedIds.includes(key)}
|
||||
disabled={picked}
|
||||
onChange={(e) => {
|
||||
if (picked) return;
|
||||
setPickSelectedIds((prev) =>
|
||||
e.target.checked
|
||||
? [...new Set([...prev, key])]
|
||||
: prev.filter((id) => id !== key),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span className="table-tag">{src}</span>
|
||||
</td>
|
||||
<td>{row.name || row.ip_address}</td>
|
||||
<td>{row.vendor}</td>
|
||||
<td>{row.ip_address}</td>
|
||||
<td>
|
||||
<span className="conn-pill conn-pill--up">{row.connect_status}</span>
|
||||
</td>
|
||||
<td className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
disabled={picked}
|
||||
onClick={() => {
|
||||
addNe(row);
|
||||
setPickSelectedIds((prev) => prev.filter((id) => id !== key));
|
||||
}}
|
||||
>
|
||||
{picked ? t("collect.create.added") : t("collect.create.add")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!eligibleQuery.isLoading && eligibleItems.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="muted">
|
||||
{t("collect.create.pickEmpty")}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{neTotal > 0 ? (
|
||||
<div className="ops-detail-modal__foot" style={{ marginTop: 10 }}>
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(neTotal),
|
||||
page: String(nePage),
|
||||
pages: String(nePages),
|
||||
})}
|
||||
</span>
|
||||
<div className="btn-row">
|
||||
<button
|
||||
type="button"
|
||||
disabled={nePage <= 1}
|
||||
onClick={() => {
|
||||
setNePage(nePage - 1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={nePage >= nePages}
|
||||
onClick={() => {
|
||||
setNePage(nePage + 1);
|
||||
setPickSelectedIds([]);
|
||||
}}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-detail-modal__foot">
|
||||
<span className="muted">
|
||||
{t("collect.create.meta", { ne: selectedIds.length, cmd: commandLines })}
|
||||
</span>
|
||||
<div className="btn-row">
|
||||
<button type="button" onClick={() => setCreateOpen(false)}>
|
||||
{t("networkConfigs.close")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
disabled={selectedIds.length === 0 || commandLines === 0 || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
{createMutation.isPending ? t("collect.create.creating") : t("collect.create.create")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{expandedJobId ? (
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
CollectionJobDetail,
|
||||
CollectionJobItem,
|
||||
CollectionDashboard,
|
||||
CollectionRunList,
|
||||
EligibleNeItem,
|
||||
IntegrationStatus,
|
||||
|
|
@ -558,6 +559,9 @@ export const fetchNeCollections = (params: { page: number; pageSize: number }) =
|
|||
);
|
||||
};
|
||||
|
||||
export const fetchCollectionDashboard = () =>
|
||||
apiGet<CollectionDashboard>("/v1/ne-collections/dashboard");
|
||||
|
||||
export const fetchCollectionJob = (jobId: string) => apiGet<CollectionJobDetail>(`/v1/ne-collections/${jobId}`);
|
||||
|
||||
export const fetchCollectionRuns = (params: {
|
||||
|
|
|
|||
|
|
@ -316,6 +316,26 @@ export type CollectionJobItem = {
|
|||
last_run_at: string | null;
|
||||
};
|
||||
|
||||
export type CollectionJobSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
ne_count: number;
|
||||
success_count: number;
|
||||
fail_count: number;
|
||||
created_at?: string | null;
|
||||
started_at?: string | null;
|
||||
ended_at?: string | null;
|
||||
last_run_at?: string | null;
|
||||
};
|
||||
|
||||
export type CollectionDashboard = {
|
||||
job_count: number;
|
||||
active_count: number;
|
||||
running_job: CollectionJobSummary | null;
|
||||
last_job: CollectionJobSummary | null;
|
||||
};
|
||||
|
||||
export type CollectionRunItem = {
|
||||
id: string;
|
||||
job_id: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue