mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-07 03:06:40 +00:00
Resource Rules page:
Split into 3 clear sections: Enabled Rules (with explanation), Rule Templates, and Resource Rules Configuration Hide Rules Configuration when rules are disabled Rule Template pages: Rules: adopt Settings section layout; right-aligned “Add Rule” button that opens a Create Rule dialog; remove inline add form; consistent table styling
This commit is contained in:
@@ -57,8 +57,7 @@ import {
|
|||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
|
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { ArrowUpDown, Check, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||||
import { ArrowUpDown, Check, InfoIcon, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
InfoSection,
|
InfoSection,
|
||||||
InfoSections,
|
InfoSections,
|
||||||
@@ -74,6 +73,15 @@ import { Switch } from "@app/components/ui/switch";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ResourceRulesManager } from "@app/components/ruleTemplate/ResourceRulesManager";
|
import { ResourceRulesManager } from "@app/components/ruleTemplate/ResourceRulesManager";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from "@app/components/ui/dialog";
|
||||||
|
|
||||||
// Schema for rule validation
|
// Schema for rule validation
|
||||||
const addRuleSchema = z.object({
|
const addRuleSchema = z.object({
|
||||||
@@ -103,6 +111,7 @@ export default function ResourceRules(props: {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 25
|
pageSize: 25
|
||||||
});
|
});
|
||||||
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -574,302 +583,63 @@ export default function ResourceRules(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{/* <Alert className="hidden md:block"> */}
|
{/* 1. Enabled Rules Control & How it works */}
|
||||||
{/* <InfoIcon className="h-4 w-4" /> */}
|
|
||||||
{/* <AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle> */}
|
|
||||||
{/* <AlertDescription className="mt-4"> */}
|
|
||||||
{/* <div className="space-y-1 mb-4"> */}
|
|
||||||
{/* <p> */}
|
|
||||||
{/* {t('rulesAboutDescription')} */}
|
|
||||||
{/* </p> */}
|
|
||||||
{/* </div> */}
|
|
||||||
{/* <InfoSections cols={2}> */}
|
|
||||||
{/* <InfoSection> */}
|
|
||||||
{/* <InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle> */}
|
|
||||||
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
|
|
||||||
{/* <li className="flex items-center gap-2"> */}
|
|
||||||
{/* <Check className="text-green-500 w-4 h-4" /> */}
|
|
||||||
{/* {t('rulesActionAlwaysAllow')} */}
|
|
||||||
{/* </li> */}
|
|
||||||
{/* <li className="flex items-center gap-2"> */}
|
|
||||||
{/* <X className="text-red-500 w-4 h-4" /> */}
|
|
||||||
{/* {t('rulesActionAlwaysDeny')} */}
|
|
||||||
{/* </li> */}
|
|
||||||
{/* </ul> */}
|
|
||||||
{/* </InfoSection> */}
|
|
||||||
{/* <InfoSection> */}
|
|
||||||
{/* <InfoSectionTitle> */}
|
|
||||||
{/* {t('rulesMatchCriteria')} */}
|
|
||||||
{/* </InfoSectionTitle> */}
|
|
||||||
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
|
|
||||||
{/* <li className="flex items-center gap-2"> */}
|
|
||||||
{/* {t('rulesMatchCriteriaIpAddress')} */}
|
|
||||||
{/* </li> */}
|
|
||||||
{/* <li className="flex items-center gap-2"> */}
|
|
||||||
{/* {t('rulesMatchCriteriaIpAddressRange')} */}
|
|
||||||
{/* </li> */}
|
|
||||||
{/* <li className="flex items-center gap-2"> */}
|
|
||||||
{/* {t('rulesMatchCriteriaUrl')} */}
|
|
||||||
{/* </li> */}
|
|
||||||
{/* </ul> */}
|
|
||||||
{/* </InfoSection> */}
|
|
||||||
{/* </InfoSections> */}
|
|
||||||
{/* </AlertDescription> */}
|
|
||||||
{/* </Alert> */}
|
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t('rulesResource')}
|
{t('rulesEnable')}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t('rulesResourceDescription')}
|
{t('rulesEnableDescription')}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<div className="space-y-6">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<SwitchInput
|
||||||
<SwitchInput
|
id="rules-toggle"
|
||||||
id="rules-toggle"
|
label={t('rulesEnable')}
|
||||||
label={t('rulesEnable')}
|
defaultChecked={rulesEnabled}
|
||||||
defaultChecked={rulesEnabled}
|
onCheckedChange={(val) => setRulesEnabled(val)}
|
||||||
onCheckedChange={(val) => setRulesEnabled(val)}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/30 p-4">
|
||||||
|
<div className="mb-3 text-sm text-muted-foreground">
|
||||||
|
{t('rulesAboutDescription')}
|
||||||
</div>
|
</div>
|
||||||
|
<InfoSections cols={2}>
|
||||||
<Form {...addRuleForm}>
|
<InfoSection>
|
||||||
<form
|
<InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle>
|
||||||
onSubmit={addRuleForm.handleSubmit(addRule)}
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
className="space-y-4"
|
<li className="flex items-center gap-2">
|
||||||
>
|
<Check className="text-green-500 w-4 h-4" />
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
|
{t('rulesActionAlwaysAllow')}
|
||||||
<FormField
|
</li>
|
||||||
control={addRuleForm.control}
|
<li className="flex items-center gap-2">
|
||||||
name="action"
|
<X className="text-red-500 w-4 h-4" />
|
||||||
render={({ field }) => (
|
{t('rulesActionAlwaysDeny')}
|
||||||
<FormItem>
|
</li>
|
||||||
<FormLabel>{t('rulesAction')}</FormLabel>
|
</ul>
|
||||||
<FormControl>
|
</InfoSection>
|
||||||
<Select
|
<InfoSection>
|
||||||
value={field.value}
|
<InfoSectionTitle>{t('rulesMatchCriteria')}</InfoSectionTitle>
|
||||||
onValueChange={
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
field.onChange
|
<li className="flex items-center gap-2">
|
||||||
}
|
{t('rulesMatchCriteriaIpAddress')}
|
||||||
>
|
</li>
|
||||||
<SelectTrigger className="w-full">
|
<li className="flex items-center gap-2">
|
||||||
<SelectValue />
|
{t('rulesMatchCriteriaIpAddressRange')}
|
||||||
</SelectTrigger>
|
</li>
|
||||||
<SelectContent>
|
<li className="flex items-center gap-2">
|
||||||
<SelectItem value="ACCEPT">
|
{t('rulesMatchCriteriaUrl')}
|
||||||
{RuleAction.ACCEPT}
|
</li>
|
||||||
</SelectItem>
|
</ul>
|
||||||
<SelectItem value="DROP">
|
</InfoSection>
|
||||||
{RuleAction.DROP}
|
</InfoSections>
|
||||||
</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>
|
|
||||||
{resource.http && (
|
|
||||||
<SelectItem value="PATH">
|
|
||||||
{RuleMatch.PATH}
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
<SelectItem value="IP">
|
|
||||||
{RuleMatch.IP}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="CIDR">
|
|
||||||
{RuleMatch.CIDR}
|
|
||||||
</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>
|
|
||||||
<Input {...field}/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={!rulesEnabled}
|
|
||||||
>
|
|
||||||
{t('ruleSubmit')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<TableHead key={header.id}>
|
|
||||||
{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) => (
|
|
||||||
<TableCell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
{t('rulesNoOne')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
{/* <TableCaption> */}
|
|
||||||
{/* {t('rulesOrder')} */}
|
|
||||||
{/* </TableCaption> */}
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
|
||||||
{rules.length > 0 && (
|
|
||||||
<div className="flex items-center justify-between space-x-2 py-4">
|
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
|
||||||
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{" "}
|
|
||||||
{Math.min(
|
|
||||||
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
|
|
||||||
table.getFilteredRowModel().rows.length
|
|
||||||
)}{" "}
|
|
||||||
of {table.getFilteredRowModel().rows.length} rules
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<p className="text-sm font-medium">Rows per page</p>
|
|
||||||
<Select
|
|
||||||
value={`${table.getState().pagination.pageSize}`}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
table.setPageSize(Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
|
||||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[10, 25, 50, 100].map((pageSize) => (
|
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
||||||
{pageSize}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
|
||||||
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
|
||||||
{table.getPageCount()}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => table.setPageIndex(0)}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to first page</span>
|
|
||||||
<ChevronsLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to previous page</span>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to next page</span>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to last page</span>
|
|
||||||
<ChevronsRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
{/* Template Assignment Section */}
|
{/* 2. Rule Templates Section */}
|
||||||
{rulesEnabled && (
|
{rulesEnabled && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
@@ -890,12 +660,238 @@ export default function ResourceRules(props: {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 3. Resource Rules Configuration */}
|
||||||
|
{rulesEnabled && (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t('rulesResource')}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t('rulesResourceDescription')}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="secondary">{t('ruleSubmit')}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('ruleSubmit')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('rulesResourceDescription')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...addRuleForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={addRuleForm.handleSubmit(async (data) => {
|
||||||
|
await addRule(data);
|
||||||
|
setCreateDialogOpen(false);
|
||||||
|
})}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
{resource.http && (
|
||||||
|
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
|
||||||
|
)}
|
||||||
|
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
||||||
|
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={addRuleForm.control}
|
||||||
|
name="value"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="gap-1 md:col-span-2">
|
||||||
|
<InfoPopup
|
||||||
|
text={t('value')}
|
||||||
|
info={getValueHelpText(addRuleForm.watch('match')) || ''}
|
||||||
|
/>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit">{t('ruleSubmit')}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{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) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{rules.length > 0 && (
|
||||||
|
<div className="flex items-center justify-between space-x-2 py-4">
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{" "}
|
||||||
|
{Math.min(
|
||||||
|
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
|
||||||
|
table.getFilteredRowModel().rows.length
|
||||||
|
)}{" "}
|
||||||
|
of {table.getFilteredRowModel().rows.length} rules
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium">Rows per page</p>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[10, 25, 50, 100].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button onClick={saveAllSettings} loading={loading} disabled={loading}>
|
||||||
onClick={saveAllSettings}
|
|
||||||
loading={loading}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t('saveAllSettings')}
|
{t('saveAllSettings')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionHeader
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
import { Textarea } from "@app/components/ui/textarea";
|
||||||
@@ -118,37 +122,44 @@ export default function GeneralPage() {
|
|||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle title={t("templateDetails")} />
|
<SettingsSectionTitle>
|
||||||
|
{t("templateDetails")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
Update the name and description for this rule template.
|
||||||
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<SettingsSectionBody>
|
||||||
<div>
|
<SettingsSectionForm>
|
||||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" id="template-general-form">
|
||||||
{t("name")}
|
<div>
|
||||||
</label>
|
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||||
<Input
|
{t("name")}
|
||||||
id="name"
|
</label>
|
||||||
{...register("name")}
|
<Input
|
||||||
className={errors.name ? "border-red-500" : ""}
|
id="name"
|
||||||
/>
|
{...register("name")}
|
||||||
{errors.name && (
|
className={errors.name ? "border-red-500" : ""}
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
|
/>
|
||||||
)}
|
{errors.name && (
|
||||||
</div>
|
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
|
||||||
<div>
|
)}
|
||||||
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
</div>
|
||||||
{t("description")}
|
<div>
|
||||||
</label>
|
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
||||||
<Textarea
|
{t("description")}
|
||||||
id="description"
|
</label>
|
||||||
{...register("description")}
|
<Textarea id="description" {...register("description")} rows={3} />
|
||||||
rows={3}
|
</div>
|
||||||
/>
|
</form>
|
||||||
</div>
|
</SettingsSectionForm>
|
||||||
<Button type="submit" disabled={saving}>
|
</SettingsSectionBody>
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button type="submit" form="template-general-form" disabled={saving}>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
{saving ? t("saving") : t("save")}
|
{saving ? t("saving") : t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,24 +4,35 @@ import { useParams } from "next/navigation";
|
|||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionHeader
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionBody
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { TemplateRulesManager } from "@app/components/ruleTemplate/TemplateRulesManager";
|
import { TemplateRulesManager } from "@app/components/ruleTemplate/TemplateRulesManager";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function RulesPage() {
|
export default function RulesPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle title="Template Rules" />
|
<SettingsSectionTitle>
|
||||||
|
{t('ruleTemplates')}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
Manage the rules for this template. Changes propagate to all assigned resources.
|
||||||
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<TemplateRulesManager
|
<SettingsSectionBody>
|
||||||
orgId={params.orgId as string}
|
<TemplateRulesManager
|
||||||
templateId={params.templateId as string}
|
orgId={params.orgId as string}
|
||||||
/>
|
templateId={params.templateId as string}
|
||||||
|
/>
|
||||||
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
@@ -156,47 +155,43 @@ export function ResourceRulesManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Template Assignment */}
|
<div className="space-y-2">
|
||||||
<Card>
|
<div className="flex items-center gap-2">
|
||||||
<CardHeader>
|
<Select
|
||||||
<CardTitle>Template Assignment</CardTitle>
|
value={selectedTemplate}
|
||||||
<CardDescription>
|
onValueChange={(value) => {
|
||||||
Assign rule templates to this resource for consistent access control
|
setSelectedTemplate(value);
|
||||||
</CardDescription>
|
handleAssignTemplate(value);
|
||||||
</CardHeader>
|
}}
|
||||||
<CardContent className="space-y-4">
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<SelectTrigger className="w-64">
|
||||||
<Select
|
<SelectValue placeholder="Select a template to assign" />
|
||||||
value={selectedTemplate}
|
</SelectTrigger>
|
||||||
onValueChange={(value) => {
|
<SelectContent>
|
||||||
setSelectedTemplate(value);
|
{templates.map((template) => (
|
||||||
handleAssignTemplate(value);
|
<SelectItem key={template.templateId} value={template.templateId}>
|
||||||
}}
|
{template.name}
|
||||||
>
|
</SelectItem>
|
||||||
<SelectTrigger className="w-64">
|
))}
|
||||||
<SelectValue placeholder="Select a template to assign" />
|
</SelectContent>
|
||||||
</SelectTrigger>
|
</Select>
|
||||||
<SelectContent>
|
</div>
|
||||||
{templates.map((template) => (
|
|
||||||
<SelectItem key={template.templateId} value={template.templateId}>
|
|
||||||
{template.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{resourceTemplates.length > 0 && (
|
{resourceTemplates.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">Assigned Templates</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-medium">Assigned Templates</h4>
|
|
||||||
{resourceTemplates.map((template) => (
|
{resourceTemplates.map((template) => (
|
||||||
<div key={template.templateId} className="flex items-center justify-between p-3 border rounded-lg">
|
<div
|
||||||
<div className="flex items-center space-x-2">
|
key={template.templateId}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-md bg-muted/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{template.name}</span>
|
<span className="font-medium">{template.name}</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
{template.description && (
|
||||||
{template.description}
|
<span className="text-sm text-muted-foreground">{template.description}</span>
|
||||||
</span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -209,9 +204,9 @@ export function ResourceRulesManager({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
open={unassignDialogOpen}
|
open={unassignDialogOpen}
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ import {
|
|||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators";
|
||||||
import { ArrowUpDown, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
import { ArrowUpDown, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||||
import { ConfirmationDialog } from "@app/components/ConfirmationDialog";
|
import { ConfirmationDialog } from "@app/components/ConfirmationDialog";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from "@app/components/ui/dialog";
|
||||||
|
|
||||||
const addRuleSchema = z.object({
|
const addRuleSchema = z.object({
|
||||||
action: z.enum(["ACCEPT", "DROP"]),
|
action: z.enum(["ACCEPT", "DROP"]),
|
||||||
@@ -76,6 +85,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager
|
|||||||
const [rules, setRules] = useState<TemplateRule[]>([]);
|
const [rules, setRules] = useState<TemplateRule[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [addingRule, setAddingRule] = useState(false);
|
const [addingRule, setAddingRule] = useState(false);
|
||||||
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 25
|
pageSize: 25
|
||||||
@@ -366,107 +376,116 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div>Loading rules...</div>;
|
return <div className="text-muted-foreground">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Form {...form}>
|
<div className="flex justify-end">
|
||||||
<form onSubmit={form.handleSubmit(addRule)} className="space-y-4">
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
|
<DialogTrigger asChild>
|
||||||
<FormField
|
<Button variant="secondary" disabled={addingRule}>
|
||||||
control={form.control}
|
{addingRule ? "Adding Rule..." : t('ruleSubmit')}
|
||||||
name="action"
|
</Button>
|
||||||
render={({ field }) => (
|
</DialogTrigger>
|
||||||
<FormItem>
|
<DialogContent>
|
||||||
<FormLabel>{t('rulesAction')}</FormLabel>
|
<DialogHeader>
|
||||||
<FormControl>
|
<DialogTitle>{t('ruleSubmit')}</DialogTitle>
|
||||||
<Select
|
<DialogDescription>
|
||||||
value={field.value}
|
{t('rulesResourceDescription')}
|
||||||
onValueChange={field.onChange}
|
</DialogDescription>
|
||||||
>
|
</DialogHeader>
|
||||||
<SelectTrigger>
|
<Form {...form}>
|
||||||
<SelectValue />
|
<form
|
||||||
</SelectTrigger>
|
onSubmit={form.handleSubmit(async (data) => {
|
||||||
<SelectContent>
|
await addRule(data);
|
||||||
<SelectItem value="ACCEPT">
|
setCreateDialogOpen(false);
|
||||||
{RuleAction.ACCEPT}
|
})}
|
||||||
</SelectItem>
|
className="space-y-4"
|
||||||
<SelectItem value="DROP">{RuleAction.DROP}</SelectItem>
|
>
|
||||||
</SelectContent>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
</Select>
|
<FormField
|
||||||
</FormControl>
|
control={form.control}
|
||||||
<FormMessage />
|
name="action"
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem>
|
||||||
/>
|
<FormLabel>{t('rulesAction')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ACCEPT">{RuleAction.ACCEPT}</SelectItem>
|
||||||
|
<SelectItem value="DROP">{RuleAction.DROP}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="match"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('rulesMatchType')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
|
||||||
|
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
||||||
|
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="value"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>{t('value')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter value" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('rulesPriority')} (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="Auto" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" variant="secondary" disabled={addingRule}>
|
||||||
|
{addingRule ? "Adding Rule..." : t('ruleSubmit')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div>
|
||||||
control={form.control}
|
|
||||||
name="match"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('rulesMatchType')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
|
|
||||||
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
|
||||||
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="value"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('value')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Enter value" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('rulesPriority')} (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Auto"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" variant="secondary" disabled={addingRule}>
|
|
||||||
{addingRule ? "Adding Rule..." : "Add Rule"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<div className="rounded-md border">
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user