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:
Adrian Astles
2025-08-08 19:30:26 +08:00
parent 16a88281bb
commit 75cec731e8
5 changed files with 498 additions and 466 deletions

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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) => (