mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Improve biz_state monitor-item table UX with select-all and visible scroll.
Keep the horizontal scrollbar in the viewport, sticky enable/title columns, and show selected/total (and unbound) counts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a1acd4c2af
commit
86fcb98218
4 changed files with 193 additions and 38 deletions
|
|
@ -239,6 +239,12 @@ const en = {
|
||||||
scheduleSaved: "Schedule saved",
|
scheduleSaved: "Schedule saved",
|
||||||
colInterval: "Interval",
|
colInterval: "Interval",
|
||||||
profiles: "Monitor items",
|
profiles: "Monitor items",
|
||||||
|
profilesSelectAll: "Select all",
|
||||||
|
profilesDeselectAll: "Deselect all",
|
||||||
|
profilesSelectAllOk: "Enabled {{n}} item(s)",
|
||||||
|
profilesDeselectAllOk: "All items disabled",
|
||||||
|
profilesSelectionStats: "Selected {{enabled}} / {{total}}",
|
||||||
|
profilesUnboundStats: "{{n}} unbound",
|
||||||
colLane: "Collect lane",
|
colLane: "Collect lane",
|
||||||
laneLight: "Light",
|
laneLight: "Light",
|
||||||
laneHeavy: "Heavy",
|
laneHeavy: "Heavy",
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,12 @@ const zh = {
|
||||||
scheduleSaved: "周期配置已保存",
|
scheduleSaved: "周期配置已保存",
|
||||||
colInterval: "周期",
|
colInterval: "周期",
|
||||||
profiles: "监控项",
|
profiles: "监控项",
|
||||||
|
profilesSelectAll: "全选",
|
||||||
|
profilesDeselectAll: "取消全选",
|
||||||
|
profilesSelectAllOk: "已启用 {{n}} 项",
|
||||||
|
profilesDeselectAllOk: "已全部取消启用",
|
||||||
|
profilesSelectionStats: "已选 {{enabled}} / 共 {{total}} 项",
|
||||||
|
profilesUnboundStats: "未关联 {{n}}",
|
||||||
colLane: "采集车道",
|
colLane: "采集车道",
|
||||||
laneLight: "轻车道",
|
laneLight: "轻车道",
|
||||||
laneHeavy: "重车道",
|
laneHeavy: "重车道",
|
||||||
|
|
|
||||||
|
|
@ -11396,13 +11396,35 @@ html.login-page--paused .login-page__flare {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-profiles-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-height: min(62vh, 620px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-profiles-table-wrap {
|
.bs-profiles-table-wrap {
|
||||||
overflow-x: scroll !important;
|
flex: 1 1 auto;
|
||||||
overflow-y: auto !important;
|
min-height: 0;
|
||||||
max-height: min(58vh, 560px);
|
overflow: auto !important;
|
||||||
|
max-height: none;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
/* Keep horizontal scrollbar gutter visible. */
|
/* Always reserve room for both scrollbars so H-bar stays in the visible viewport. */
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable both-edges;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.65) rgba(15, 23, 42, 0.9);
|
scrollbar-color: rgba(148, 163, 184, 0.65) rgba(15, 23, 42, 0.9);
|
||||||
}
|
}
|
||||||
|
|
@ -11428,8 +11450,8 @@ html.login-page--paused .login-page__flare {
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table {
|
.bs-profiles-table {
|
||||||
width: 100%;
|
width: max-content;
|
||||||
min-width: 1180px;
|
min-width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
|
@ -11443,9 +11465,10 @@ html.login-page--paused .login-page__flare {
|
||||||
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.25);
|
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* enable | title | lane | actions | params | command | aux */
|
||||||
.bs-profiles-table th:nth-child(1),
|
.bs-profiles-table th:nth-child(1),
|
||||||
.bs-profiles-table td:nth-child(1) {
|
.bs-profiles-table td:nth-child(1) {
|
||||||
width: 48px;
|
width: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(2),
|
.bs-profiles-table th:nth-child(2),
|
||||||
|
|
@ -11455,22 +11478,53 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(3),
|
.bs-profiles-table th:nth-child(3),
|
||||||
.bs-profiles-table td:nth-child(3) {
|
.bs-profiles-table td:nth-child(3) {
|
||||||
width: 200px;
|
width: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(4),
|
.bs-profiles-table th:nth-child(4),
|
||||||
.bs-profiles-table td:nth-child(4) {
|
.bs-profiles-table td:nth-child(4) {
|
||||||
width: 100px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(5),
|
.bs-profiles-table th:nth-child(5),
|
||||||
.bs-profiles-table td:nth-child(5) {
|
.bs-profiles-table td:nth-child(5) {
|
||||||
width: 280px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(6),
|
.bs-profiles-table th:nth-child(6),
|
||||||
.bs-profiles-table td:nth-child(6) {
|
.bs-profiles-table td:nth-child(6) {
|
||||||
width: auto;
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table th:nth-child(7),
|
||||||
|
.bs-profiles-table td:nth-child(7) {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-sticky-col {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(15, 23, 42, 0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table thead .bs-profiles-sticky-col,
|
||||||
|
.bs-profiles-table thead th:nth-child(1),
|
||||||
|
.bs-profiles-table thead th:nth-child(2) {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-sticky-col--1,
|
||||||
|
.bs-profiles-table th:nth-child(1),
|
||||||
|
.bs-profiles-table td.bs-profiles-sticky-col--1 {
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 1px 0 0 rgba(148, 163, 184, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-sticky-col--2,
|
||||||
|
.bs-profiles-table th:nth-child(2),
|
||||||
|
.bs-profiles-table td.bs-profiles-sticky-col--2 {
|
||||||
|
left: 52px;
|
||||||
|
box-shadow: 1px 0 0 rgba(148, 163, 184, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table .bs-cmd-cell {
|
.bs-profiles-table .bs-cmd-cell {
|
||||||
|
|
@ -14450,17 +14504,23 @@ option {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Biz-state profiles: keep wide table + visible H-scroll (overrides modal fill rules). */
|
/* Biz-state profiles: keep wide table + H-scroll inside a fixed viewport. */
|
||||||
|
.app-heroui-modal__dialog [data-slot="modal-body"] > .bs-profiles-panel,
|
||||||
|
.app-heroui-modal__dialog .modal__body > .bs-profiles-panel {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.app-heroui-modal .pt-list-table-wrap.bs-profiles-table-wrap {
|
.app-heroui-modal .pt-list-table-wrap.bs-profiles-table-wrap {
|
||||||
overflow-x: scroll !important;
|
overflow: auto !important;
|
||||||
overflow-y: auto !important;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-heroui-modal .pt-list-table.bs-profiles-table {
|
.app-heroui-modal .pt-list-table.bs-profiles-table {
|
||||||
width: 100% !important;
|
width: max-content !important;
|
||||||
min-width: 960px !important;
|
min-width: 100% !important;
|
||||||
table-layout: fixed !important;
|
table-layout: fixed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,28 @@ export function BizStatePage() {
|
||||||
[profiles],
|
[profiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const profileSelectionStats = useMemo(() => {
|
||||||
|
const items = (detail?.items || []) as Array<{
|
||||||
|
source_profile_id?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
bindings?: unknown[];
|
||||||
|
}>;
|
||||||
|
const byId = new Map(items.map((it) => [String(it.source_profile_id || ""), it]));
|
||||||
|
let enabled = 0;
|
||||||
|
let unbound = 0;
|
||||||
|
for (const prof of collectProfiles) {
|
||||||
|
const it = byId.get(prof.profile_id);
|
||||||
|
if (!it?.enabled) continue;
|
||||||
|
enabled += 1;
|
||||||
|
if ((prof.placeholders || []).length > 0) {
|
||||||
|
const binds = (it.bindings || []) as Array<{ value?: string }>;
|
||||||
|
const hasVal = binds.some((b) => String(b.value || "").trim());
|
||||||
|
if (!hasVal) unbound += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { total: collectProfiles.length, enabled, unbound };
|
||||||
|
}, [collectProfiles, detail?.items]);
|
||||||
|
|
||||||
const sheetTabs = useMemo(
|
const sheetTabs = useMemo(
|
||||||
() => (batchDetail ? buildSheetTabs(batchDetail, t) : []),
|
() => (batchDetail ? buildSheetTabs(batchDetail, t) : []),
|
||||||
[batchDetail, t],
|
[batchDetail, t],
|
||||||
|
|
@ -951,6 +973,40 @@ export function BizStatePage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAllProfilesEnabled = async (enable: boolean) => {
|
||||||
|
if (!taskId || !detail || !collectProfiles.length) return;
|
||||||
|
const items = [...(detail.items || [])] as any[];
|
||||||
|
const byId = new Map(items.map((it, i) => [String(it.source_profile_id || ""), i]));
|
||||||
|
for (const prof of collectProfiles) {
|
||||||
|
const idx = byId.get(prof.profile_id);
|
||||||
|
if (idx != null) {
|
||||||
|
items[idx] = { ...items[idx], enabled: enable };
|
||||||
|
} else if (enable) {
|
||||||
|
items.push({
|
||||||
|
source_profile_id: prof.profile_id,
|
||||||
|
kind: "catalog",
|
||||||
|
enabled: true,
|
||||||
|
title: prof.title,
|
||||||
|
bindings: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await bizStatePatchTask(taskId, { items });
|
||||||
|
await loadTask(taskId);
|
||||||
|
showOk(
|
||||||
|
enable
|
||||||
|
? t("bizState.profilesSelectAllOk", { n: collectProfiles.length })
|
||||||
|
: t("bizState.profilesDeselectAllOk"),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeBindModal = () => {
|
const closeBindModal = () => {
|
||||||
setBindItemId("");
|
setBindItemId("");
|
||||||
setCandidates([]);
|
setCandidates([]);
|
||||||
|
|
@ -1650,7 +1706,34 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{taskTab === "profiles" ? (
|
{taskTab === "profiles" ? (
|
||||||
<>
|
<div className="bs-profiles-panel">
|
||||||
|
<div className="btn-row bs-profiles-toolbar">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
isDisabled={busy || !collectProfiles.length}
|
||||||
|
onPress={() => void setAllProfilesEnabled(true)}
|
||||||
|
>
|
||||||
|
{t("bizState.profilesSelectAll")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy || !profileSelectionStats.enabled}
|
||||||
|
onPress={() => void setAllProfilesEnabled(false)}
|
||||||
|
>
|
||||||
|
{t("bizState.profilesDeselectAll")}
|
||||||
|
</Button>
|
||||||
|
<span className="muted bs-profiles-stats">
|
||||||
|
{t("bizState.profilesSelectionStats", {
|
||||||
|
enabled: profileSelectionStats.enabled,
|
||||||
|
total: profileSelectionStats.total,
|
||||||
|
})}
|
||||||
|
{profileSelectionStats.unbound
|
||||||
|
? ` · ${t("bizState.profilesUnboundStats", { n: profileSelectionStats.unbound })}`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="pt-list-table-wrap bs-profiles-table-wrap">
|
<div className="pt-list-table-wrap bs-profiles-table-wrap">
|
||||||
<table className="data-table pt-list-table bs-profiles-table">
|
<table className="data-table pt-list-table bs-profiles-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -1658,8 +1741,8 @@ export function BizStatePage() {
|
||||||
<th>{t("bizState.enable")}</th>
|
<th>{t("bizState.enable")}</th>
|
||||||
<th>{t("bizState.profiles")}</th>
|
<th>{t("bizState.profiles")}</th>
|
||||||
<th>{t("bizState.colLane")}</th>
|
<th>{t("bizState.colLane")}</th>
|
||||||
<th>{t("bizState.params")}</th>
|
|
||||||
<th>{t("bizState.colActions")}</th>
|
<th>{t("bizState.colActions")}</th>
|
||||||
|
<th>{t("bizState.params")}</th>
|
||||||
<th>{t("bizState.command")}</th>
|
<th>{t("bizState.command")}</th>
|
||||||
<th>{t("bizState.colAux")}</th>
|
<th>{t("bizState.colAux")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1710,7 +1793,7 @@ export function BizStatePage() {
|
||||||
: "—";
|
: "—";
|
||||||
return (
|
return (
|
||||||
<tr key={prof.profile_id}>
|
<tr key={prof.profile_id}>
|
||||||
<td>
|
<td className="bs-profiles-sticky-col bs-profiles-sticky-col--1">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
|
|
@ -1718,7 +1801,7 @@ export function BizStatePage() {
|
||||||
onChange={(e) => void toggleProfileItem(prof, e.target.checked)}
|
onChange={(e) => void toggleProfileItem(prof, e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="bs-profiles-sticky-col bs-profiles-sticky-col--2">
|
||||||
<div className="pt-list-task-name">{prof.title}</div>
|
<div className="pt-list-task-name">{prof.title}</div>
|
||||||
{prof.description ? <div className="muted">{prof.description}</div> : null}
|
{prof.description ? <div className="muted">{prof.description}</div> : null}
|
||||||
{needsBind ? (
|
{needsBind ? (
|
||||||
|
|
@ -1736,6 +1819,22 @@ export function BizStatePage() {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="bs-actions-cell">
|
||||||
|
{needsBind && enabled && it ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
isDisabled={busy || discoverLoading}
|
||||||
|
onPress={() => void startDiscover(it)}
|
||||||
|
>
|
||||||
|
{t("bizState.discoverBind")}
|
||||||
|
</Button>
|
||||||
|
) : needsBind ? (
|
||||||
|
<span className="muted">—</span>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="bs-params-cell">
|
<td className="bs-params-cell">
|
||||||
{needsBind ? (
|
{needsBind ? (
|
||||||
<div
|
<div
|
||||||
|
|
@ -1761,22 +1860,6 @@ export function BizStatePage() {
|
||||||
"—"
|
"—"
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="bs-actions-cell">
|
|
||||||
{needsBind && enabled && it ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
isDisabled={busy || discoverLoading}
|
|
||||||
onPress={() => void startDiscover(it)}
|
|
||||||
>
|
|
||||||
{t("bizState.discoverBind")}
|
|
||||||
</Button>
|
|
||||||
) : needsBind ? (
|
|
||||||
<span className="muted">—</span>
|
|
||||||
) : (
|
|
||||||
"—"
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<code className="bs-cmd-cell" title={prof.command_template}>
|
<code className="bs-cmd-cell" title={prof.command_template}>
|
||||||
{prof.command_template}
|
{prof.command_template}
|
||||||
|
|
@ -1809,7 +1892,7 @@ export function BizStatePage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<div className="btn-row" style={{ marginBottom: 8, gap: 8, flexWrap: "wrap" }}>
|
<div className="btn-row" style={{ marginBottom: 8, gap: 8, flexWrap: "wrap" }}>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue