diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 585dbea..db6445d 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -239,6 +239,12 @@ const en = { scheduleSaved: "Schedule saved", colInterval: "Interval", 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", laneLight: "Light", laneHeavy: "Heavy", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 8e0f5cf..b13dc66 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -239,6 +239,12 @@ const zh = { scheduleSaved: "周期配置已保存", colInterval: "周期", profiles: "监控项", + profilesSelectAll: "全选", + profilesDeselectAll: "取消全选", + profilesSelectAllOk: "已启用 {{n}} 项", + profilesDeselectAllOk: "已全部取消启用", + profilesSelectionStats: "已选 {{enabled}} / 共 {{total}} 项", + profilesUnboundStats: "未关联 {{n}}", colLane: "采集车道", laneLight: "轻车道", laneHeavy: "重车道", diff --git a/web/src/index.css b/web/src/index.css index 69ee732..aec5011 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11396,13 +11396,35 @@ html.login-page--paused .login-page__flare { 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 { - overflow-x: scroll !important; - overflow-y: auto !important; - max-height: min(58vh, 560px); + flex: 1 1 auto; + min-height: 0; + overflow: auto !important; + max-height: none; max-width: 100%; - /* Keep horizontal scrollbar gutter visible. */ - scrollbar-gutter: stable; + /* Always reserve room for both scrollbars so H-bar stays in the visible viewport. */ + scrollbar-gutter: stable both-edges; scrollbar-width: thin; 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 { - width: 100%; - min-width: 1180px; + width: max-content; + min-width: 100%; table-layout: fixed; border-collapse: separate; 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); } +/* enable | title | lane | actions | params | command | aux */ .bs-profiles-table th:nth-child(1), .bs-profiles-table td:nth-child(1) { - width: 48px; + width: 52px; } .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 td:nth-child(3) { - width: 200px; + width: 88px; } .bs-profiles-table th:nth-child(4), .bs-profiles-table td:nth-child(4) { - width: 100px; + width: 120px; } .bs-profiles-table th:nth-child(5), .bs-profiles-table td:nth-child(5) { - width: 280px; + width: 200px; } .bs-profiles-table th: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 { @@ -14450,17 +14504,23 @@ option { 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 { - overflow-x: scroll !important; - overflow-y: auto !important; + overflow: auto !important; max-width: 100%; width: 100%; + max-height: none; } .app-heroui-modal .pt-list-table.bs-profiles-table { - width: 100% !important; - min-width: 960px !important; + width: max-content !important; + min-width: 100% !important; table-layout: fixed !important; } diff --git a/web/src/pages/network/BizStatePage.tsx b/web/src/pages/network/BizStatePage.tsx index debd54a..4ce238c 100644 --- a/web/src/pages/network/BizStatePage.tsx +++ b/web/src/pages/network/BizStatePage.tsx @@ -455,6 +455,28 @@ export function BizStatePage() { [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( () => (batchDetail ? buildSheetTabs(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 = () => { setBindItemId(""); setCandidates([]); @@ -1650,7 +1706,34 @@ export function BizStatePage() { {taskTab === "profiles" ? ( - <> +
+
+ + + + {t("bizState.profilesSelectionStats", { + enabled: profileSelectionStats.enabled, + total: profileSelectionStats.total, + })} + {profileSelectionStats.unbound + ? ` · ${t("bizState.profilesUnboundStats", { n: profileSelectionStats.unbound })}` + : ""} + +
@@ -1658,8 +1741,8 @@ export function BizStatePage() { - + @@ -1710,7 +1793,7 @@ export function BizStatePage() { : "—"; return ( - - +
{t("bizState.enable")} {t("bizState.profiles")} {t("bizState.colLane")}{t("bizState.params")} {t("bizState.colActions")}{t("bizState.params")} {t("bizState.command")} {t("bizState.colAux")}
+ void toggleProfileItem(prof, e.target.checked)} /> +
{prof.title}
{prof.description ?
{prof.description}
: null} {needsBind ? ( @@ -1736,6 +1819,22 @@ export function BizStatePage() { )}
+ {needsBind && enabled && it ? ( + + ) : needsBind ? ( + — + ) : ( + "—" + )} + {needsBind ? (
-
- {needsBind && enabled && it ? ( - - ) : needsBind ? ( - — - ) : ( - "—" - )} - {prof.command_template} @@ -1809,7 +1892,7 @@ export function BizStatePage() {
- +
) : (