mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +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";
|
||||
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { ArrowUpDown, Check, InfoIcon, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||
import { ArrowUpDown, Check, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSections,
|
||||
@@ -74,6 +73,15 @@ import { Switch } from "@app/components/ui/switch";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ResourceRulesManager } from "@app/components/ruleTemplate/ResourceRulesManager";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@app/components/ui/dialog";
|
||||
|
||||
// Schema for rule validation
|
||||
const addRuleSchema = z.object({
|
||||
@@ -103,6 +111,7 @@ export default function ResourceRules(props: {
|
||||
pageIndex: 0,
|
||||
pageSize: 25
|
||||
});
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -574,302 +583,63 @@ export default function ResourceRules(props: {
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{/* <Alert className="hidden md:block"> */}
|
||||
{/* <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> */}
|
||||
|
||||
{/* 1. Enabled Rules Control & How it works */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('rulesResource')}
|
||||
{t('rulesEnable')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('rulesResourceDescription')}
|
||||
{t('rulesEnableDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<SwitchInput
|
||||
id="rules-toggle"
|
||||
label={t('rulesEnable')}
|
||||
defaultChecked={rulesEnabled}
|
||||
onCheckedChange={(val) => setRulesEnabled(val)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<SwitchInput
|
||||
id="rules-toggle"
|
||||
label={t('rulesEnable')}
|
||||
defaultChecked={rulesEnabled}
|
||||
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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Template Assignment Section */}
|
||||
{/* 2. Rule Templates Section */}
|
||||
{rulesEnabled && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
@@ -881,21 +651,247 @@ export default function ResourceRules(props: {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<ResourceRulesManager
|
||||
resourceId={params.resourceId.toString()}
|
||||
orgId={resource.orgId}
|
||||
<ResourceRulesManager
|
||||
resourceId={params.resourceId.toString()}
|
||||
orgId={resource.orgId}
|
||||
onUpdate={fetchRules}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</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">
|
||||
<Button
|
||||
onClick={saveAllSettings}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
<Button onClick={saveAllSettings} loading={loading} disabled={loading}>
|
||||
{t('saveAllSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,13 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionHeader
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm
|
||||
} from "@app/components/Settings";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Textarea } from "@app/components/ui/textarea";
|
||||
@@ -118,37 +122,44 @@ export default function GeneralPage() {
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle title={t("templateDetails")} />
|
||||
<SettingsSectionTitle>
|
||||
{t("templateDetails")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Update the name and description for this rule template.
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
{t("name")}
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
{...register("name")}
|
||||
className={errors.name ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
||||
{t("description")}
|
||||
</label>
|
||||
<Textarea
|
||||
id="description"
|
||||
{...register("description")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={saving}>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" id="template-general-form">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
{t("name")}
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
{...register("name")}
|
||||
className={errors.name ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
||||
{t("description")}
|
||||
</label>
|
||||
<Textarea id="description" {...register("description")} rows={3} />
|
||||
</div>
|
||||
</form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button type="submit" form="template-general-form" disabled={saving}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{saving ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
|
||||
@@ -4,24 +4,35 @@ import { useParams } from "next/navigation";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionHeader
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody
|
||||
} from "@app/components/Settings";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { TemplateRulesManager } from "@app/components/ruleTemplate/TemplateRulesManager";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function RulesPage() {
|
||||
const params = useParams();
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle title="Template Rules" />
|
||||
<SettingsSectionTitle>
|
||||
{t('ruleTemplates')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Manage the rules for this template. Changes propagate to all assigned resources.
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<TemplateRulesManager
|
||||
orgId={params.orgId as string}
|
||||
templateId={params.templateId as string}
|
||||
/>
|
||||
<SettingsSectionBody>
|
||||
<TemplateRulesManager
|
||||
orgId={params.orgId as string}
|
||||
templateId={params.templateId as string}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
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 { useToast } from "@app/hooks/useToast";
|
||||
import { Trash2 } from "lucide-react";
|
||||
@@ -156,47 +155,43 @@ export function ResourceRulesManager({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Template Assignment */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Template Assignment</CardTitle>
|
||||
<CardDescription>
|
||||
Assign rule templates to this resource for consistent access control
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTemplate(value);
|
||||
handleAssignTemplate(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-64">
|
||||
<SelectValue placeholder="Select a template to assign" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.templateId} value={template.templateId}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTemplate(value);
|
||||
handleAssignTemplate(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-64">
|
||||
<SelectValue placeholder="Select a template to assign" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{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">
|
||||
<h4 className="font-medium">Assigned Templates</h4>
|
||||
{resourceTemplates.map((template) => (
|
||||
<div key={template.templateId} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
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="text-sm text-muted-foreground">
|
||||
{template.description}
|
||||
</span>
|
||||
{template.description && (
|
||||
<span className="text-sm text-muted-foreground">{template.description}</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -209,9 +204,9 @@ export function ResourceRulesManager({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={unassignDialogOpen}
|
||||
|
||||
@@ -47,6 +47,15 @@ import {
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators";
|
||||
import { ArrowUpDown, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||
import { ConfirmationDialog } from "@app/components/ConfirmationDialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@app/components/ui/dialog";
|
||||
|
||||
const addRuleSchema = z.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]),
|
||||
@@ -76,6 +85,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager
|
||||
const [rules, setRules] = useState<TemplateRule[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [addingRule, setAddingRule] = useState(false);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 25
|
||||
@@ -366,107 +376,116 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading rules...</div>;
|
||||
return <div className="text-muted-foreground">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(addRule)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="action"
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" disabled={addingRule}>
|
||||
{addingRule ? "Adding Rule..." : t('ruleSubmit')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('ruleSubmit')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('rulesResourceDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.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={form.control}
|
||||
name="action"
|
||||
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
|
||||
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">
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
||||
Reference in New Issue
Block a user