From 6d5eff88fad0d8786b817f82dacb3a1925d92779 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:58:43 +0800 Subject: [PATCH] Security rules: drop the group checkboxes; inline selects no longer clip text in WebKit The group header rows keep their name and enabled count but lose the bulk on/off checkbox, along with the bulk write path behind it. Inline native selects had 4px vertical padding inside a 24px box, which WebKit uses to clip the 20px line from the bottom. Drop the vertical padding and tuck the chevron to the right edge so it no longer covers the last character. Co-Authored-By: Claude Opus 5.5 --- src/security/GuardTab.i18n.tsx | 3 --- src/security/GuardTab.tsx | 28 +++--------------------- src/security/SecurityPage.i18n.tsx | 4 ---- src/security/SecurityPage.tsx | 35 ------------------------------ src/security/api.ts | 3 --- src/security/rules.test.ts | 16 -------------- src/security/rules.ts | 13 +---------- src/ui/native-select.tsx | 7 +++--- 8 files changed, 8 insertions(+), 101 deletions(-) diff --git a/src/security/GuardTab.i18n.tsx b/src/security/GuardTab.i18n.tsx index bba14364..9794cadf 100644 --- a/src/security/GuardTab.i18n.tsx +++ b/src/security/GuardTab.i18n.tsx @@ -34,8 +34,6 @@ export const guardTabText = messages( /** 组标题:「隐藏字符 · 内置 · 已启用 2 条,共 3 条」 */ builtin: "内置", groupCount: (on: number, all: number) => `已启用 ${on} 条,共 ${all} 条`, - /** 组标题的复选框 */ - groupToggle: (group: string) => `启用「${group}」中的全部规则`, /** 行内的动作选择,给读屏 */ actionFor: (name: string) => `「${name}」的动作`, /** 动作为「替换」时,行内选项写出换成什么 */ @@ -70,7 +68,6 @@ export const guardTabText = messages( action: "Action", builtin: "Built-in", groupCount: (on: number, all: number) => `${on} of ${all} on`, - groupToggle: (group: string) => `Turn on all rules in “${group}”`, actionFor: (name: string) => `Action for “${name}”`, replaceWith: (text: string) => `Replace with ${text}`, turnOn: "Turn on", diff --git a/src/security/GuardTab.tsx b/src/security/GuardTab.tsx index 079c84cc..13be914e 100644 --- a/src/security/GuardTab.tsx +++ b/src/security/GuardTab.tsx @@ -2,7 +2,6 @@ import { Fragment, useMemo, useState } from "react"; import { FlaskConicalIcon, PlusIcon, SearchIcon } from "lucide-react"; import { Badge } from "@/ui/badge"; import { Button } from "@/ui/button"; -import { Checkbox } from "@/ui/checkbox"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/ui/input-group"; import { rowMotion, usePresentList } from "@/ui/motion"; import { NativeSelect, NativeSelectOption } from "@/ui/native-select"; @@ -27,9 +26,7 @@ import { actionAllowed, actionOf, DEFAULT_REPLACEMENT, - groupChecked, groupRules, - groupTarget, ruleMatches, scopeChoice, scopeOf, @@ -43,8 +40,6 @@ const MODES: readonly GuardMode[] = ["enforce", "observe", "off"]; export interface RuleActions { mode: (mode: GuardMode) => void; toggle: (r: SecurityRuleView, enabled: boolean) => void; - /** 一组规则一起启用或停用(组标题的复选框) */ - bulk: (rules: SecurityRuleView[], enabled: boolean) => void; /** 行内改一条规则的动作 */ setAction: (r: SecurityRuleView, action: RuleAction) => void; /** 这条规则正在写(开关、动作) */ @@ -131,7 +126,7 @@ export function GuardSkeleton() { * 规则的实际处理按这个顺序定(规格 §4.3):防护关闭不检查;规则停用不检查;防护处于 * 「监控」时一律仅记录(动作列照写用户设的动作,后面标「监控中」);其余按规则的动作。 * - * 内置规则按类别分组,组标题上的复选框一次启用或停用这一组(部分启用时半选);自定义 + * 内置规则按类别分组,组标题只写名字和启用条数、没有开关;自定义 * 规则在最后一组。检索框按名字和写法筛。每一行的开关、动作就地改;点一行打开它的对话框。 */ export function GuardTab({ @@ -290,8 +285,6 @@ function RuleTable({ title={title} builtin={!g.custom && g.key !== "command"} rules={live} - pending={live.some(actions.pending)} - onToggle={(enabled) => actions.bulk(live, enabled)} /> {g.rows.map(({ item: r, key, presence }) => ( void; }) { const t = useText(guardTabText); const on = rules.filter((r) => r.enabled).length; return ( - - onToggle(groupTarget(rules))} - /> - - + {title} {builtin && ` · ${t.builtin}`} {` · ${t.groupCount(on, rules.length)}`} diff --git a/src/security/SecurityPage.i18n.tsx b/src/security/SecurityPage.i18n.tsx index 7b6d8b9d..eb7c0893 100644 --- a/src/security/SecurityPage.i18n.tsx +++ b/src/security/SecurityPage.i18n.tsx @@ -36,8 +36,6 @@ export const securityPageText = messages( /** 撤销提示 */ ruleOn: (name: string) => `已启用「${name}」`, ruleOff: (name: string) => `已停用「${name}」`, - groupOn: (n: number) => `已启用 ${n} 条规则`, - groupOff: (n: number) => `已停用 ${n} 条规则`, actionSet: (name: string, action: string) => `「${name}」的动作已改为「${action}」`, modeSet: (guard: string, mode: string) => `${guard}已切换为「${mode}」`, ruleCreated: (name: string) => `已创建规则「${name}」`, @@ -64,8 +62,6 @@ export const securityPageText = messages( loadFailed: "The security settings could not be loaded", ruleOn: (name: string) => `“${name}” turned on`, ruleOff: (name: string) => `“${name}” turned off`, - groupOn: (n: number) => (n === 1 ? "1 rule turned on" : `${n} rules turned on`), - groupOff: (n: number) => (n === 1 ? "1 rule turned off" : `${n} rules turned off`), actionSet: (name: string, action: string) => `Action for “${name}” set to ${action}`, modeSet: (guard: string, mode: string) => `${guard} set to ${mode}`, ruleCreated: (name: string) => `Rule “${name}” created`, diff --git a/src/security/SecurityPage.tsx b/src/security/SecurityPage.tsx index 793f4d38..50c752e7 100644 --- a/src/security/SecurityPage.tsx +++ b/src/security/SecurityPage.tsx @@ -183,40 +183,6 @@ export default function SecurityPage({ }); } - /** - * 一组规则一起启用或停用(组标题的复选框)。只写状态要变的那几条,一次写配置;撤销把 - * 这几条拨回去 - */ - function bulk(guard: Guard, rules: SecurityRuleView[], enabled: boolean) { - const targets = rules.filter((r) => r.enabled !== enabled); - if (targets.length === 0) return; - const hit = (x: SecurityRuleView) => targets.some((r) => same(r, x)); - const send = (on: boolean) => - tracked( - targets.map((r) => ruleKey(guard, r)), - () => - write((base) => - api.setEnabled(guard, { - builtin: targets.filter((r) => !r.custom).map((r) => r.id), - custom: targets.filter((r) => r.custom).map((r) => r.id), - enabled: on, - base_version: base, - }), - ), - ); - void undoable({ - message: enabled ? t.groupOn(targets.length) : t.groupOff(targets.length), - apply: () => - detail.mutate((d) => ({ - ...d!, - [guard]: { ...d![guard], rules: d![guard].rules.map((x) => (hit(x) ? { ...x, enabled } : x)) }, - })), - do: () => send(enabled), - undo: () => send(!enabled), - after: settle, - }); - } - /** 行内改一条规则的动作。可撤销 */ function setAction(guard: Guard, r: SecurityRuleView, action: RuleAction) { const before = actionOf(guard, r); @@ -259,7 +225,6 @@ export default function SecurityPage({ const actions = (guard: Guard): RuleActions => ({ mode: (mode) => setMode(guard, mode), toggle: (r, enabled) => toggle(guard, r, enabled), - bulk: (rules, enabled) => bulk(guard, rules, enabled), setAction: (r, a) => setAction(guard, r, a), pending: (r) => busy(ruleKey(guard, r)), open: (r) => diff --git a/src/security/api.ts b/src/security/api.ts index e18d9694..62886345 100644 --- a/src/security/api.ts +++ b/src/security/api.ts @@ -13,7 +13,6 @@ import type { Guard, GuardMode, RuleAction, - RulesToggle, SecurityEventsQuery, } from "@/types"; @@ -56,8 +55,6 @@ export const api = { /** 启用或停用一条内置规则 */ toggleBuiltin: (guard: Guard, id: string, enabled: boolean, baseVersion: string) => call("ToggleBuiltinRule", { enabled, base_version: baseVersion }, guard, id), - /** 一组规则一起启用或停用:一次写配置 */ - setEnabled: (guard: Guard, save: RulesToggle) => call("SetRulesEnabled", save, guard), /** 一条内置规则在「开启」档下做什么 */ setAction: (guard: Guard, id: string, action: RuleAction, baseVersion: string) => call("SetBuiltinRuleAction", { action, base_version: baseVersion }, guard, id), diff --git a/src/security/rules.test.ts b/src/security/rules.test.ts index 4e56b6d1..86638353 100644 --- a/src/security/rules.test.ts +++ b/src/security/rules.test.ts @@ -4,9 +4,7 @@ import { actionAllowed, actionOf, adjustForScope, - groupChecked, groupRules, - groupTarget, matchAllowed, patternText, ruleMatches, @@ -44,20 +42,6 @@ describe("分组", () => { }); }); -describe("组标题的复选框", () => { - it("全部启用、部分启用(半选)、全部停用", () => { - expect(groupChecked([{ enabled: true }, { enabled: true }])).toBe(true); - expect(groupChecked([{ enabled: true }, { enabled: false }])).toBe("indeterminate"); - expect(groupChecked([{ enabled: false }])).toBe(false); - }); - - it("全部启用时点它全部停用,否则全部启用", () => { - expect(groupTarget([{ enabled: true }, { enabled: true }])).toBe(false); - expect(groupTarget([{ enabled: true }, { enabled: false }])).toBe(true); - expect(groupTarget([{ enabled: false }])).toBe(true); - }); -}); - describe("检索", () => { it("按界面上的名字、英文名、id 和写法找,不分大小写", () => { const r = rule({ diff --git a/src/security/rules.ts b/src/security/rules.ts index 7534bfff..e0811183 100644 --- a/src/security/rules.ts +++ b/src/security/rules.ts @@ -1,5 +1,5 @@ /** - * 规则表和规则对话框里不画东西的那部分:分组、检索、组标题复选框的状态、内容规则 + * 规则表和规则对话框里不画东西的那部分:分组、检索、内容规则 * 「检查对象」和「动作」「匹配方式」的联动。拆出来是为了能单独测。 */ import type { ContentMatch, ContentScope, Guard, OutcomeDetail, RuleAction, SecurityRuleView } from "@/types"; @@ -39,17 +39,6 @@ export function groupRules(rows: readonly T[], ruleOf: (row: T) => SecurityRu return out; } -/** 组标题复选框:全部启用、部分启用(半选)、全部停用 */ -export function groupChecked(rules: readonly Pick[]): boolean | "indeterminate" { - const on = rules.filter((r) => r.enabled).length; - return on === 0 ? false : on === rules.length ? true : "indeterminate"; -} - -/** 点组标题复选框之后要的状态:全部启用时全部停用,否则全部启用 */ -export function groupTarget(rules: readonly Pick[]): boolean { - return groupChecked(rules) !== true; -} - /** * 一条规则写的是什么,拿来检索:正则、要找的文字、码位、前缀、域名后缀。代码里做的检查 * 和没有写法的内置规则(PEM、JWT…)是空的,只按名字找得到 diff --git a/src/ui/native-select.tsx b/src/ui/native-select.tsx index 8855ec20..82de228a 100644 --- a/src/ui/native-select.tsx +++ b/src/ui/native-select.tsx @@ -1,5 +1,6 @@ // 多了一档 `inline`,和 `Input`、`SelectTrigger` 的同名档配套:上游那张 -// 表一行要就地改六个字段,平时不显边框、鼠标移上去才显。 +// 表一行要就地改六个字段,平时不显边框、鼠标移上去才显。高 24px 不留上下内边距 +// (WebKit 会把 20px 的行切掉底),箭头贴右,不压最后一个字。 import * as React from "react" import { cn } from "@/lib/utils" @@ -26,10 +27,10 @@ function NativeSelect({