mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 12:19:50 +00:00
first pass restyle of auth methods and rules
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user