first pass restyle of auth methods and rules

This commit is contained in:
miloschwartz
2026-06-05 21:04:03 -07:00
parent 8ee520dbb5
commit dd8bcbb3e3
28 changed files with 3583 additions and 6773 deletions

View File

@@ -11,13 +11,15 @@ import {
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import z from "zod";
import { createPolicySchema, type PolicyFormValues } from ".";
import { createPolicyRulesSectionSchema, type PolicyFormValues } from ".";
import { toast } from "@app/hooks/useToast";
import {
validatePolicyRulePriority,
validatePolicyRuleValue
} from "./policy-access-rule-validation";
import { SwitchInput } from "@app/components/SwitchInput";
import { Button } from "@app/components/ui/button";
import { DataTableEmptyState } from "@app/components/ui/data-table-empty-state";
import {
Command,
CommandEmpty,
@@ -26,15 +28,6 @@ import {
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { InfoPopup } from "@app/components/ui/info-popup";
import { Input } from "@app/components/ui/input";
import {
Popover,
@@ -60,11 +53,6 @@ import {
import { MAJOR_ASNS } from "@server/db/asns";
import { COUNTRIES } from "@server/db/countries";
import {
isValidCIDR,
isValidIP,
isValidUrlGlobPattern
} from "@server/lib/validators";
import {
ColumnDef,
flexRender,
@@ -79,14 +67,10 @@ import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { type UseFormReturn, useForm, useWatch } from "react-hook-form";
// ─── CreatePolicyRulesSectionForm ─────────────────────────────────────────────
import { PolicyAccessRulesIntro } from "./PolicyAccessRulesIntro";
import { createEmptyRule } from "./policy-access-rule-utils";
const addRuleSchema = z.object({
action: z.enum(["ACCEPT", "DROP", "PASS"]),
match: z.string(),
value: z.string(),
priority: z.coerce.number<number>().int().optional()
});
// ─── CreatePolicyRulesSectionForm ─────────────────────────────────────────────
type LocalRule = {
ruleId: number;
@@ -111,19 +95,15 @@ export function CreatePolicyRulesSectionForm({
isMaxmindAsnAvailable
}: CreatePolicyRulesSectionFormProps) {
const t = useTranslations();
const [isExpanded, setIsExpanded] = useState(false);
const [rules, setRules] = useState<LocalRule[]>([]);
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
const rulesFormSchema = useMemo(
() => createPolicyRulesSectionSchema(t),
[t]
);
const form = useForm({
resolver: zodResolver(
createPolicySchema.pick({
applyRules: true,
rules: true
})
),
resolver: zodResolver(rulesFormSchema),
defaultValues: {
applyRules: false,
rules: []
@@ -143,15 +123,6 @@ export function CreatePolicyRulesSectionForm({
name: "applyRules"
});
const addRuleForm = useForm({
resolver: zodResolver(addRuleSchema),
defaultValues: {
action: "ACCEPT" as const,
match: "PATH",
value: ""
}
});
const RuleAction = useMemo(
() => ({
ACCEPT: t("alwaysAllow"),
@@ -190,84 +161,11 @@ export function CreatePolicyRulesSectionForm({
[form]
);
const addRule = useCallback(
function addRule(data: z.infer<typeof addRuleSchema>) {
const isDuplicate = rules.some(
(rule) =>
rule.action === data.action &&
rule.match === data.match &&
rule.value === data.value
);
if (isDuplicate) {
toast({
variant: "destructive",
title: t("rulesErrorDuplicate"),
description: t("rulesErrorDuplicateDescription")
});
return;
}
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidIpAddressRange"),
description: t("rulesErrorInvalidIpAddressRangeDescription")
});
return;
}
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidUrl"),
description: t("rulesErrorInvalidUrlDescription")
});
return;
}
if (data.match === "IP" && !isValidIP(data.value)) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidIpAddress"),
description: t("rulesErrorInvalidIpAddressDescription")
});
return;
}
if (
data.match === "COUNTRY" &&
!COUNTRIES.some((c) => c.code === data.value)
) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidCountry"),
description: t("rulesErrorInvalidCountryDescription") || ""
});
return;
}
let priority = data.priority;
if (priority === undefined) {
priority =
rules.reduce(
(acc, rule) =>
rule.priority > acc ? rule.priority : acc,
0
) + 1;
}
const updatedRules = [
...rules,
{
...data,
ruleId: new Date().getTime(),
new: true,
priority,
enabled: true
}
];
setRules(updatedRules);
syncFormRules(updatedRules);
addRuleForm.reset();
},
[rules, t, addRuleForm, syncFormRules]
);
const addEmptyRule = useCallback(() => {
const updatedRules = [...rules, createEmptyRule(rules)];
setRules(updatedRules);
syncFormRules(updatedRules);
}, [rules, syncFormRules]);
const removeRule = useCallback(
function removeRule(ruleId: number) {
@@ -291,63 +189,63 @@ export function CreatePolicyRulesSectionForm({
[rules, syncFormRules]
);
const getValueHelpText = useCallback(
function getValueHelpText(type: string) {
switch (type) {
case "CIDR":
return t("rulesMatchIpAddressRangeDescription");
case "IP":
return t("rulesMatchIpAddress");
case "PATH":
return t("rulesMatchUrl");
case "COUNTRY":
return t("rulesMatchCountry");
case "ASN":
return "Enter an Autonomous System Number (e.g., AS15169 or 15169)";
}
},
[t]
);
const columns: ColumnDef<LocalRule>[] = useMemo(
() => [
{
accessorKey: "priority",
size: 96,
maxSize: 96,
header: ({ column }) => (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("rulesPriority")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
<div className="p-3">
<Button
variant="ghost"
className="h-auto p-0 font-medium text-muted-foreground hover:bg-transparent"
onClick={() =>
column.toggleSorting(
column.getIsSorted() === "asc"
)
}
>
{t("rulesPriority")}
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
</div>
),
cell: ({ row }) => (
<Input
defaultValue={row.original.priority}
className="w-[75px]"
className="w-full min-w-0"
type="number"
onClick={(e) => e.currentTarget.focus()}
onBlur={(e) => {
const parsed = z.coerce
.number()
.int()
.optional()
.safeParse(e.target.value);
if (!parsed.success) {
const validated = validatePolicyRulePriority(
t,
e.target.value
);
if (!validated.success) {
toast({
variant: "destructive",
title: t("rulesErrorInvalidPriority"),
...validated.toast
});
return;
}
const duplicatePriority = rules.some(
(rule) =>
rule.ruleId !== row.original.ruleId &&
rule.priority === validated.data
);
if (duplicatePriority) {
toast({
variant: "destructive",
title: t("rulesErrorDuplicatePriority"),
description: t(
"rulesErrorInvalidPriorityDescription"
"rulesErrorDuplicatePriorityDescription"
)
});
return;
}
updateRule(row.original.ruleId, {
priority: parsed.data
priority: validated.data
});
}}
/>
@@ -355,6 +253,8 @@ export function CreatePolicyRulesSectionForm({
},
{
accessorKey: "action",
size: 160,
maxSize: 160,
header: () => <span className="p-3">{t("rulesAction")}</span>,
cell: ({ row }) => (
<Select
@@ -363,7 +263,7 @@ export function CreatePolicyRulesSectionForm({
updateRule(row.original.ruleId, { action: value })
}
>
<SelectTrigger className="min-w-[150px]">
<SelectTrigger className="h-8 w-full min-w-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -382,6 +282,8 @@ export function CreatePolicyRulesSectionForm({
},
{
accessorKey: "match",
size: 144,
maxSize: 144,
header: () => (
<span className="p-3">{t("rulesMatchType")}</span>
),
@@ -402,7 +304,7 @@ export function CreatePolicyRulesSectionForm({
})
}
>
<SelectTrigger className="min-w-[125px]">
<SelectTrigger className="h-8 w-full min-w-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -577,11 +479,23 @@ export function CreatePolicyRulesSectionForm({
<Input
defaultValue={row.original.value}
className="min-w-50"
onBlur={(e) =>
onBlur={(e) => {
const validated = validatePolicyRuleValue(
t,
row.original.match,
e.target.value
);
if (!validated.success) {
toast({
variant: "destructive",
...validated.toast
});
return;
}
updateRule(row.original.ruleId, {
value: e.target.value
})
}
value: validated.data
});
}}
/>
)
},
@@ -589,19 +503,23 @@ export function CreatePolicyRulesSectionForm({
accessorKey: "enabled",
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
onCheckedChange={(val) =>
updateRule(row.original.ruleId, { enabled: val })
}
/>
<div className="flex items-center w-full">
<Switch
defaultChecked={row.original.enabled}
onCheckedChange={(val) =>
updateRule(row.original.ruleId, {
enabled: val
})
}
/>
</div>
)
},
{
id: "actions",
header: () => <span className="p-3">{t("actions")}</span>,
header: () => null,
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<div className="flex items-center justify-end space-x-2">
<Button
variant="outline"
onClick={() => removeRule(row.original.ruleId)}
@@ -619,7 +537,8 @@ export function CreatePolicyRulesSectionForm({
isMaxmindAvailable,
isMaxmindAsnAvailable,
updateRule,
removeRule
removeRule,
rules
]
);
@@ -633,36 +552,18 @@ export function CreatePolicyRulesSectionForm({
state: { pagination: { pageIndex: 0, pageSize: 1000 } }
});
if (!isExpanded) {
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("rulesResource")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("rulesResourcePolicyDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Button
type="button"
variant="outline"
onClick={() => setIsExpanded(true)}
>
<Plus className="mr-2 h-4 w-4" />
{t("resourcePolicyRulesAdd")}
</Button>
</SettingsSectionBody>
</SettingsSection>
);
}
const addRuleButton = (
<Button type="button" variant="outline" onClick={addEmptyRule}>
<Plus className="h-4 w-4 mr-2" />
{t("ruleSubmit")}
</Button>
);
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("rulesResource")}
{t("policyAccessRulesTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("rulesResourceDescription")}
@@ -670,421 +571,128 @@ export function CreatePolicyRulesSectionForm({
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="flex flex-col gap-y-6 pb-20">
<div className="flex items-center gap-x-2">
<SwitchInput
id="rules-toggle"
label={t("rulesEnable")}
defaultChecked={false}
onCheckedChange={(val) => {
form.setValue("applyRules", val);
}}
/>
</div>
<PolicyAccessRulesIntro
rulesEnabled={Boolean(rulesEnabled)}
onRulesEnabledChange={(val) => {
form.setValue("applyRules", val);
}}
/>
<Form {...addRuleForm}>
<form
onSubmit={addRuleForm.handleSubmit(addRule)}
className="space-y-4"
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
<FormField
control={addRuleForm.control}
name="action"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("rulesAction")}
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={
field.onChange
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACCEPT">
{RuleAction.ACCEPT}
</SelectItem>
<SelectItem value="DROP">
{RuleAction.DROP}
</SelectItem>
<SelectItem value="PASS">
{RuleAction.PASS}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addRuleForm.control}
name="match"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("rulesMatchType")}
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={
field.onChange
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="PATH">
{RuleMatch.PATH}
</SelectItem>
<SelectItem value="IP">
{RuleMatch.IP}
</SelectItem>
<SelectItem value="CIDR">
{RuleMatch.CIDR}
</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="COUNTRY">
{
RuleMatch.COUNTRY
{rulesEnabled && (
<>
<Table>
<TableHeader>
{table
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(
(header) => {
const columnId =
header.column.id;
const isActionsColumn =
columnId ===
"actions";
const isPriorityColumn =
columnId ===
"priority";
const isActionColumn =
columnId ===
"action";
const isMatchColumn =
columnId ===
"match";
return (
<TableHead
key={header.id}
className={
isActionsColumn
? "sticky right-0 z-10 w-[1%] min-w-fit bg-card text-right"
: isPriorityColumn
? "w-24 max-w-24"
: isActionColumn
? "w-40 max-w-40"
: isMatchColumn
? "w-36 max-w-36"
: ""
}
</SelectItem>
)}
{isMaxmindAsnAvailable && (
<SelectItem value="ASN">
{RuleMatch.ASN}
</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addRuleForm.control}
name="value"
render={({ field }) => (
<FormItem className="gap-1">
<InfoPopup
text={t("value")}
info={
getValueHelpText(
addRuleForm.watch(
"match"
)
) || ""
}
/>
<FormControl>
{addRuleForm.watch("match") ===
"COUNTRY" ? (
<Popover
open={
openAddRuleCountrySelect
}
onOpenChange={
setOpenAddRuleCountrySelect
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={
openAddRuleCountrySelect
}
className="w-full justify-between"
>
{field.value
? COUNTRIES.find(
(c) =>
c.code ===
field.value
)?.name +
" (" +
field.value +
")"
: t(
"selectCountry"
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput
placeholder={t(
"searchCountries"
)}
/>
<CommandList>
<CommandEmpty>
{t(
"noCountryFound"
)}
</CommandEmpty>
<CommandGroup>
{COUNTRIES.map(
(
country
) => (
<CommandItem
key={
country.code
}
value={
country.name
}
onSelect={() => {
field.onChange(
country.code
);
setOpenAddRuleCountrySelect(
false
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${field.value === country.code ? "opacity-100" : "opacity-0"}`}
/>
{
country.name
}{" "}
(
{
country.code
}
)
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : addRuleForm.watch(
"match"
) === "ASN" ? (
<Popover
open={
openAddRuleAsnSelect
}
onOpenChange={
setOpenAddRuleAsnSelect
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={
openAddRuleAsnSelect
}
className="w-full justify-between"
>
{field.value
? MAJOR_ASNS.find(
(
asn
) =>
asn.code ===
field.value
)?.name +
" (" +
field.value +
")" ||
field.value
: "Select ASN"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Search ASNs or enter custom..." />
<CommandList>
<CommandEmpty>
No ASN
found.
Use the
custom
input
below.
</CommandEmpty>
<CommandGroup>
{MAJOR_ASNS.map(
(
asn
) => (
<CommandItem
key={
asn.code
}
value={
asn.name +
" " +
asn.code
}
onSelect={() => {
field.onChange(
asn.code
);
setOpenAddRuleAsnSelect(
false
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${field.value === asn.code ? "opacity-100" : "opacity-0"}`}
/>
{
asn.name
}{" "}
(
{
asn.code
}
)
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
<div className="border-t p-2">
<Input
placeholder="Enter custom ASN (e.g., AS15169)"
onKeyDown={(
e
) => {
if (
e.key ===
"Enter"
) {
const value =
e.currentTarget.value
.toUpperCase()
.replace(
/^AS/,
""
);
if (
/^\d+$/.test(
value
)
) {
field.onChange(
"AS" +
value
);
setOpenAddRuleAsnSelect(
false
);
}
}
}}
className="text-sm"
/>
</div>
</PopoverContent>
</Popover>
) : (
<Input {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="outline"
disabled={!rulesEnabled}
>
{t("ruleSubmit")}
</Button>
</div>
</form>
</Form>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const isActionsColumn =
header.column.id === "actions";
return (
<TableHead
key={header.id}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{header.isPlaceholder
? null
: flexRender(
header.column
.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => {
const isActionsColumn =
cell.column.id === "actions";
return (
<TableCell
key={cell.id}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
</TableHead>
);
}
>
{flexRender(
cell.column.columnDef
.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{t("rulesNoOne")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row
.getVisibleCells()
.map((cell) => {
const columnId =
cell.column.id;
const isActionsColumn =
columnId ===
"actions";
const isPriorityColumn =
columnId ===
"priority";
const isActionColumn =
columnId ===
"action";
const isMatchColumn =
columnId ===
"match";
return (
<TableCell
key={cell.id}
className={
isActionsColumn
? "sticky right-0 z-10 w-[1%] min-w-fit bg-card text-right"
: isPriorityColumn
? "w-24 max-w-24"
: isActionColumn
? "w-40 max-w-40"
: isMatchColumn
? "w-36 max-w-36"
: ""
}
>
{flexRender(
cell.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (
<DataTableEmptyState
colSpan={columns.length}
message={t("rulesNoOne")}
action={addRuleButton}
/>
)}
</TableBody>
</Table>
{table.getRowModel().rows?.length > 0 &&
addRuleButton}
</>
)}
</div>
</SettingsSectionBody>
</SettingsSection>