Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/security/GuardTab.i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}」的动作`,
/** 动作为「替换」时,行内选项写出换成什么 */
Expand Down Expand Up @@ -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",
Expand Down
28 changes: 3 additions & 25 deletions src/security/GuardTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,9 +26,7 @@ import {
actionAllowed,
actionOf,
DEFAULT_REPLACEMENT,
groupChecked,
groupRules,
groupTarget,
ruleMatches,
scopeChoice,
scopeOf,
Expand All @@ -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;
/** 这条规则正在写(开关、动作) */
Expand Down Expand Up @@ -131,7 +126,7 @@ export function GuardSkeleton() {
* 规则的实际处理按这个顺序定(规格 §4.3):防护关闭不检查;规则停用不检查;防护处于
* 「监控」时一律仅记录(动作列照写用户设的动作,后面标「监控中」);其余按规则的动作。
*
* 内置规则按类别分组,组标题上的复选框一次启用或停用这一组(部分启用时半选);自定义
* 内置规则按类别分组,组标题只写名字和启用条数、没有开关;自定义
* 规则在最后一组。检索框按名字和写法筛。每一行的开关、动作就地改;点一行打开它的对话框。
*/
export function GuardTab({
Expand Down Expand Up @@ -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 }) => (
<RuleRow
Expand All @@ -312,39 +305,24 @@ function RuleTable({
);
}

/**
* 一组的标题行:复选框一次启用或停用组内(检索筛出来的)全部规则,部分启用时半选;后面是
* 「隐藏字符 · 内置 · 已启用 2 条,共 3 条」。**复选框只是批量操作的入口**,组本身没有开关
*/
/** 一组的标题行:「隐藏字符 · 内置 · 已启用 2 条,共 3 条」。组本身没有开关,逐条启停 */
function GroupHead({
span,
title,
builtin,
rules,
pending,
onToggle,
}: {
span: number;
title: string;
/** 标题后面写「内置」(工具调用的内置组本来就叫「内置」,不重复) */
builtin: boolean;
rules: SecurityRuleView[];
pending: boolean;
onToggle: (enabled: boolean) => void;
}) {
const t = useText(guardTabText);
const on = rules.filter((r) => r.enabled).length;
return (
<TableRow className="hover:bg-transparent" data-slot="rule-group">
<TableCell className="h-8 bg-surface/70 py-0">
<Checkbox
checked={groupChecked(rules)}
disabled={pending || rules.length === 0}
aria-label={t.groupToggle(title)}
onCheckedChange={() => onToggle(groupTarget(rules))}
/>
</TableCell>
<TableCell colSpan={span - 1} className="h-8 bg-surface/70 py-0 tw-label text-muted-foreground">
<TableCell colSpan={span} className="h-8 bg-surface/70 py-0 tw-label text-muted-foreground">
<span className="font-medium text-foreground/80">{title}</span>
{builtin && ` · ${t.builtin}`}
{` · ${t.groupCount(on, rules.length)}`}
Expand Down
4 changes: 0 additions & 4 deletions src/security/SecurityPage.i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}」`,
Expand All @@ -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`,
Expand Down
35 changes: 0 additions & 35 deletions src/security/SecurityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) =>
Expand Down
3 changes: 0 additions & 3 deletions src/security/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
Guard,
GuardMode,
RuleAction,
RulesToggle,
SecurityEventsQuery,
} from "@/types";

Expand Down Expand Up @@ -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),
Expand Down
16 changes: 0 additions & 16 deletions src/security/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import {
actionAllowed,
actionOf,
adjustForScope,
groupChecked,
groupRules,
groupTarget,
matchAllowed,
patternText,
ruleMatches,
Expand Down Expand Up @@ -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({
Expand Down
13 changes: 1 addition & 12 deletions src/security/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* 规则表和规则对话框里不画东西的那部分:分组、检索、组标题复选框的状态、内容规则
* 规则表和规则对话框里不画东西的那部分:分组、检索、内容规则
* 「检查对象」和「动作」「匹配方式」的联动。拆出来是为了能单独测。
*/
import type { ContentMatch, ContentScope, Guard, OutcomeDetail, RuleAction, SecurityRuleView } from "@/types";
Expand Down Expand Up @@ -39,17 +39,6 @@ export function groupRules<T>(rows: readonly T[], ruleOf: (row: T) => SecurityRu
return out;
}

/** 组标题复选框:全部启用、部分启用(半选)、全部停用 */
export function groupChecked(rules: readonly Pick<SecurityRuleView, "enabled">[]): 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<SecurityRuleView, "enabled">[]): boolean {
return groupChecked(rules) !== true;
}

/**
* 一条规则写的是什么,拿来检索:正则、要找的文字、码位、前缀、域名后缀。代码里做的检查
* 和没有写法的内置规则(PEM、JWT…)是空的,只按名字找得到
Expand Down
7 changes: 4 additions & 3 deletions src/ui/native-select.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// 多了一档 `inline`,和 `Input`、`SelectTrigger` 的同名档配套:上游那张
// 表一行要就地改六个字段,平时不显边框、鼠标移上去才显。
// 表一行要就地改六个字段,平时不显边框、鼠标移上去才显。高 24px 不留上下内边距
// (WebKit 会把 20px 的行切掉底),箭头贴右,不压最后一个字。
import * as React from "react"
import { cn } from "@/lib/utils"

Expand All @@ -26,10 +27,10 @@ function NativeSelect({
<select
data-slot="native-select"
data-size={size}
className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 data-[size=inline]:h-6 data-[size=inline]:rounded-md data-[size=inline]:border-transparent data-[size=inline]:bg-transparent data-[size=inline]:pl-1 data-[size=inline]:pr-6 data-[size=inline]:hover:border-input dark:data-[size=inline]:bg-transparent dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 data-[size=inline]:h-6 data-[size=inline]:py-0 data-[size=inline]:rounded-md data-[size=inline]:border-transparent data-[size=inline]:bg-transparent data-[size=inline]:pl-1 data-[size=inline]:pr-5 data-[size=inline]:hover:border-input dark:data-[size=inline]:bg-transparent dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
{...props}
/>
<ChevronDownIcon className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
<ChevronDownIcon className="pointer-events-none absolute top-1/2 right-2.5 size-4 group-data-[size=inline]/native-select:right-1 -translate-y-1/2 text-muted-foreground select-none"
aria-hidden="true"
data-slot="native-select-icon" />
</div>
Expand Down
Loading