This commit is contained in:
Owen
2025-10-04 18:36:44 -07:00
parent 3123f858bb
commit c2c907852d
320 changed files with 35785 additions and 2984 deletions

View File

@@ -58,6 +58,9 @@ import {
SelectValue
} from "@app/components/ui/select";
import { Separator } from "@app/components/ui/separator";
import { build } from "@server/build";
import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext";
import { TierId } from "@server/lib/private/billing/tiers";
const UsersRolesFormSchema = z.object({
roles: z.array(
@@ -94,6 +97,9 @@ export default function ResourceAuthenticationPage() {
const router = useRouter();
const t = useTranslations();
const subscription = usePrivateSubscriptionStatusContext();
const subscribed = subscription?.getTier() === TierId.STANDARD;
const [pageLoading, setPageLoading] = useState(true);
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
@@ -178,7 +184,7 @@ export default function ResourceAuthenticationPage() {
AxiosResponse<{
idps: { idpId: number; name: string }[];
}>
>("/idp")
>(build === "saas" ? `/org/${org?.org.orgId}/idp` : "/idp")
]);
setAllRoles(
@@ -223,12 +229,23 @@ export default function ResourceAuthenticationPage() {
}))
);
setAllIdps(
idpsResponse.data.data.idps.map((idp) => ({
id: idp.idpId,
text: idp.name
}))
);
if (build === "saas") {
if (subscribed) {
setAllIdps(
idpsResponse.data.data.idps.map((idp) => ({
id: idp.idpId,
text: idp.name
}))
);
}
} else {
setAllIdps(
idpsResponse.data.data.idps.map((idp) => ({
id: idp.idpId,
text: idp.name
}))
);
}
if (
autoLoginEnabled &&

View File

@@ -79,6 +79,7 @@ import {
import { ContainersSelector } from "@app/components/ContainersSelector";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import HealthCheckDialog from "@/components/HealthCheckDialog";
import { DockerManager, DockerState } from "@app/lib/docker";
import { Container } from "@server/routers/site";
import {
@@ -98,50 +99,64 @@ import {
} from "@app/components/ui/command";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { HeadersInput } from "@app/components/HeadersInput";
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { Badge } from "@app/components/ui/badge";
const addTargetSchema = z.object({
ip: z.string().refine(isTargetValid),
method: z.string().nullable(),
port: z.coerce.number().int().positive(),
siteId: z.number().int().positive(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable()
}).refine(
(data) => {
// If path is provided, pathMatchType must be provided
if (data.path && !data.pathMatchType) {
return false;
}
// If pathMatchType is provided, path must be provided
if (data.pathMatchType && !data.path) {
return false;
}
// Validate path based on pathMatchType
if (data.path && data.pathMatchType) {
switch (data.pathMatchType) {
case "exact":
case "prefix":
// Path should start with /
return data.path.startsWith("/");
case "regex":
// Validate regex
try {
new RegExp(data.path);
return true;
} catch {
return false;
}
const addTargetSchema = z
.object({
ip: z.string().refine(isTargetValid),
method: z.string().nullable(),
port: z.coerce.number().int().positive(),
siteId: z.number().int().positive(),
path: z.string().optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
.optional()
.nullable(),
rewritePath: z.string().optional().nullable(),
rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable()
})
.refine(
(data) => {
// If path is provided, pathMatchType must be provided
if (data.path && !data.pathMatchType) {
return false;
}
// If pathMatchType is provided, path must be provided
if (data.pathMatchType && !data.path) {
return false;
}
// Validate path based on pathMatchType
if (data.path && data.pathMatchType) {
switch (data.pathMatchType) {
case "exact":
case "prefix":
// Path should start with /
return data.path.startsWith("/");
case "regex":
// Validate regex
try {
new RegExp(data.path);
return true;
} catch {
return false;
}
}
}
return true;
},
{
message: "Invalid path configuration"
}
return true;
},
{
message: "Invalid path configuration"
}
)
)
.refine(
(data) => {
// If rewritePath is provided, rewritePathType must be provided
@@ -229,6 +244,10 @@ export default function ReverseProxyTargets(props: {
const [proxySettingsLoading, setProxySettingsLoading] = useState(false);
const [pageLoading, setPageLoading] = useState(true);
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
useState<LocalTarget | null>(null);
const router = useRouter();
const proxySettingsSchema = z.object({
@@ -246,7 +265,9 @@ export default function ReverseProxyTargets(props: {
message: t("proxyErrorInvalidHeader")
}
),
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable()
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable()
});
const tlsSettingsSchema = z.object({
@@ -280,7 +301,7 @@ export default function ReverseProxyTargets(props: {
path: null,
pathMatchType: null,
rewritePath: null,
rewritePathType: null,
rewritePathType: null
} as z.infer<typeof addTargetSchema>
});
@@ -463,7 +484,21 @@ export default function ReverseProxyTargets(props: {
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: resource.resourceId
resourceId: resource.resourceId,
hcEnabled: false,
hcPath: null,
hcMethod: null,
hcInterval: null,
hcTimeout: null,
hcHeaders: null,
hcScheme: null,
hcHostname: null,
hcPort: null,
hcFollowRedirects: null,
hcHealth: "unknown",
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null
};
setTargets([...targets, newTarget]);
@@ -474,7 +509,7 @@ export default function ReverseProxyTargets(props: {
path: null,
pathMatchType: null,
rewritePath: null,
rewritePathType: null,
rewritePathType: null
});
}
@@ -494,16 +529,36 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site?.type || null
}
...target,
...data,
updated: true,
siteType: site?.type || null
}
: target
)
);
}
function updateTargetHealthCheck(targetId: number, config: any) {
setTargets(
targets.map((target) =>
target.targetId === targetId
? {
...target,
...config,
updated: true
}
: target
)
);
}
const openHealthCheckDialog = (target: LocalTarget) => {
console.log(target);
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
};
async function saveAllSettings() {
try {
setTargetsLoading(true);
@@ -518,6 +573,17 @@ export default function ReverseProxyTargets(props: {
method: target.method,
enabled: target.enabled,
siteId: target.siteId,
hcEnabled: target.hcEnabled,
hcPath: target.hcPath || null,
hcScheme: target.hcScheme || null,
hcHostname: target.hcHostname || null,
hcPort: target.hcPort || null,
hcInterval: target.hcInterval || null,
hcTimeout: target.hcTimeout || null,
hcHeaders: target.hcHeaders || null,
hcFollowRedirects: target.hcFollowRedirects || null,
hcMethod: target.hcMethod || null,
hcStatus: target.hcStatus || null,
path: target.path,
pathMatchType: target.pathMatchType,
rewritePath: target.rewritePath,
@@ -598,16 +664,20 @@ export default function ReverseProxyTargets(props: {
accessorKey: "path",
header: t("matchPath"),
cell: ({ row }) => {
const hasPathMatch = !!(row.original.path || row.original.pathMatchType);
const hasPathMatch = !!(
row.original.path || row.original.pathMatchType
);
return hasPathMatch ? (
<div className="flex items-center gap-1">
<PathMatchModal
value={{
path: row.original.path,
pathMatchType: row.original.pathMatchType,
pathMatchType: row.original.pathMatchType
}}
onChange={(config) => updateTarget(row.original.targetId, config)}
onChange={(config) =>
updateTarget(row.original.targetId, config)
}
trigger={
<Button
variant="outline"
@@ -616,7 +686,8 @@ export default function ReverseProxyTargets(props: {
<PathMatchDisplay
value={{
path: row.original.path,
pathMatchType: row.original.pathMatchType,
pathMatchType:
row.original.pathMatchType
}}
/>
</Button>
@@ -646,9 +717,11 @@ export default function ReverseProxyTargets(props: {
<PathMatchModal
value={{
path: row.original.path,
pathMatchType: row.original.pathMatchType,
pathMatchType: row.original.pathMatchType
}}
onChange={(config) => updateTarget(row.original.targetId, config)}
onChange={(config) =>
updateTarget(row.original.targetId, config)
}
trigger={
<Button variant="outline">
<Plus className="h-4 w-4 mr-2" />
@@ -657,7 +730,7 @@ export default function ReverseProxyTargets(props: {
}
/>
);
},
}
},
{
accessorKey: "siteId",
@@ -693,7 +766,7 @@ export default function ReverseProxyTargets(props: {
className={cn(
"justify-between flex-1",
!row.original.siteId &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
{row.original.siteId
@@ -772,31 +845,31 @@ export default function ReverseProxyTargets(props: {
},
...(resource.http
? [
{
accessorKey: "method",
header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select
defaultValue={row.original.method ?? ""}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger>
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)
}
]
{
accessorKey: "method",
header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select
defaultValue={row.original.method ?? ""}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger>
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)
}
]
: []),
{
accessorKey: "ip",
@@ -860,8 +933,11 @@ export default function ReverseProxyTargets(props: {
accessorKey: "rewritePath",
header: t("rewritePath"),
cell: ({ row }) => {
const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType);
const noPathMatch = !row.original.path && !row.original.pathMatchType;
const hasRewritePath = !!(
row.original.rewritePath || row.original.rewritePathType
);
const noPathMatch =
!row.original.path && !row.original.pathMatchType;
return hasRewritePath && !noPathMatch ? (
<div className="flex items-center gap-1">
@@ -869,9 +945,11 @@ export default function ReverseProxyTargets(props: {
<PathRewriteModal
value={{
rewritePath: row.original.rewritePath,
rewritePathType: row.original.rewritePathType,
rewritePathType: row.original.rewritePathType
}}
onChange={(config) => updateTarget(row.original.targetId, config)}
onChange={(config) =>
updateTarget(row.original.targetId, config)
}
trigger={
<Button
variant="outline"
@@ -880,8 +958,10 @@ export default function ReverseProxyTargets(props: {
>
<PathRewriteDisplay
value={{
rewritePath: row.original.rewritePath,
rewritePathType: row.original.rewritePathType,
rewritePath:
row.original.rewritePath,
rewritePathType:
row.original.rewritePathType
}}
/>
</Button>
@@ -896,7 +976,7 @@ export default function ReverseProxyTargets(props: {
updateTarget(row.original.targetId, {
...row.original,
rewritePath: null,
rewritePathType: null,
rewritePathType: null
});
}}
>
@@ -907,9 +987,11 @@ export default function ReverseProxyTargets(props: {
<PathRewriteModal
value={{
rewritePath: row.original.rewritePath,
rewritePathType: row.original.rewritePathType,
rewritePathType: row.original.rewritePathType
}}
onChange={(config) => updateTarget(row.original.targetId, config)}
onChange={(config) =>
updateTarget(row.original.targetId, config)
}
trigger={
<Button variant="outline" disabled={noPathMatch}>
<Plus className="h-4 w-4 mr-2" />
@@ -919,7 +1001,7 @@ export default function ReverseProxyTargets(props: {
disabled={noPathMatch}
/>
);
},
}
},
// {
@@ -940,6 +1022,79 @@ export default function ReverseProxyTargets(props: {
// </Select>
// ),
// },
{
accessorKey: "healthCheck",
header: t("healthCheck"),
cell: ({ row }) => {
const status = row.original.hcHealth || "unknown";
const isEnabled = row.original.hcEnabled;
const getStatusColor = (status: string) => {
switch (status) {
case "healthy":
return "green";
case "unhealthy":
return "red";
case "unknown":
default:
return "secondary";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "healthy":
return t("healthCheckHealthy");
case "unhealthy":
return t("healthCheckUnhealthy");
case "unknown":
default:
return t("healthCheckUnknown");
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "healthy":
return <CircleCheck className="w-3 h-3" />;
case "unhealthy":
return <CircleX className="w-3 h-3" />;
case "unknown":
default:
return null;
}
};
return (
<>
{row.original.siteType === "newt" ? (
<div className="flex items-center space-x-1">
<Badge variant={getStatusColor(status)}>
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Badge>
<Button
variant="outline"
size="sm"
onClick={() =>
openHealthCheckDialog(row.original)
}
className="h-6 w-6 p-0"
>
<Settings className="h-3 w-3" />
</Button>
</div>
) : (
<span className="text-sm text-muted-foreground">
{t("healthCheckNotAvailable")}
</span>
)}
</>
);
}
},
{
accessorKey: "enabled",
header: t("enabled"),
@@ -1034,21 +1189,21 @@ export default function ReverseProxyTargets(props: {
className={cn(
"justify-between flex-1",
!field.value &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(
site
) =>
site.siteId ===
field.value
)
?.name
(
site
) =>
site.siteId ===
field.value
)
?.name
: t(
"siteSelect"
)}
"siteSelect"
)}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
@@ -1114,34 +1269,34 @@ export default function ReverseProxyTargets(props: {
);
return selectedSite &&
selectedSite.type ===
"newt"
"newt"
? (() => {
const dockerState =
getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={
selectedSite
}
containers={
dockerState.containers
}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelect
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()
const dockerState =
getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={
selectedSite
}
containers={
dockerState.containers
}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelect
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()
: null;
})()}
</div>
@@ -1369,12 +1524,12 @@ export default function ReverseProxyTargets(props: {
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
)
)}
@@ -1544,9 +1699,7 @@ export default function ReverseProxyTargets(props: {
</FormLabel>
<FormControl>
<HeadersInput
value={
field.value
}
value={field.value}
onChange={(value) => {
field.onChange(
value
@@ -1588,6 +1741,56 @@ export default function ReverseProxyTargets(props: {
{t("saveSettings")}
</Button>
</div>
{selectedTargetForHealthCheck && (
<HealthCheckDialog
open={healthCheckDialogOpen}
setOpen={setHealthCheckDialogOpen}
targetId={selectedTargetForHealthCheck.targetId}
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
targetMethod={
selectedTargetForHealthCheck.method || undefined
}
initialConfig={{
hcEnabled:
selectedTargetForHealthCheck.hcEnabled || false,
hcPath: selectedTargetForHealthCheck.hcPath || "/",
hcMethod:
selectedTargetForHealthCheck.hcMethod || "GET",
hcInterval:
selectedTargetForHealthCheck.hcInterval || 5,
hcTimeout: selectedTargetForHealthCheck.hcTimeout || 5,
hcHeaders:
selectedTargetForHealthCheck.hcHeaders || undefined,
hcScheme:
selectedTargetForHealthCheck.hcScheme || undefined,
hcHostname:
selectedTargetForHealthCheck.hcHostname ||
selectedTargetForHealthCheck.ip,
hcPort:
selectedTargetForHealthCheck.hcPort ||
selectedTargetForHealthCheck.port,
hcFollowRedirects:
selectedTargetForHealthCheck.hcFollowRedirects ||
true,
hcStatus:
selectedTargetForHealthCheck.hcStatus || undefined,
hcMode: selectedTargetForHealthCheck.hcMode || "http",
hcUnhealthyInterval:
selectedTargetForHealthCheck.hcUnhealthyInterval ||
30
}}
onChanges={async (config) => {
if (selectedTargetForHealthCheck) {
console.log(config);
updateTargetHealthCheck(
selectedTargetForHealthCheck.targetId,
config
);
}
}}
/>
)}
</SettingsContainer>
);
}

View File

@@ -58,7 +58,7 @@ import {
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 } from "lucide-react";
import { ArrowUpDown, Check, InfoIcon, X, ChevronsUpDown } from "lucide-react";
import {
InfoSection,
InfoSections,
@@ -73,6 +73,20 @@ import {
import { Switch } from "@app/components/ui/switch";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { COUNTRIES } from "@server/db/countries";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
// Schema for rule validation
const addRuleSchema = z.object({
@@ -98,9 +112,13 @@ export default function ResourceRules(props: {
const [loading, setLoading] = useState(false);
const [pageLoading, setPageLoading] = useState(true);
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
const [openCountrySelect, setOpenCountrySelect] = useState(false);
const [countrySelectValue, setCountrySelectValue] = useState("");
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
const router = useRouter();
const t = useTranslations();
const env = useEnvContext();
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0;
const RuleAction = {
ACCEPT: t('alwaysAllow'),
@@ -111,7 +129,8 @@ export default function ResourceRules(props: {
const RuleMatch = {
PATH: t('path'),
IP: "IP",
CIDR: t('ipAddressRange')
CIDR: t('ipAddressRange'),
GEOIP: t('country')
} as const;
const addRuleForm = useForm({
@@ -193,6 +212,15 @@ export default function ResourceRules(props: {
setLoading(false);
return;
}
if (data.match === "GEOIP" && !COUNTRIES.some(c => c.code === data.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidCountry'),
description: t('rulesErrorInvalidCountryDescription') || "Invalid country code."
});
setLoading(false);
return;
}
// find the highest priority and add one
let priority = data.priority;
@@ -242,6 +270,8 @@ export default function ResourceRules(props: {
return t('rulesMatchIpAddress');
case "PATH":
return t('rulesMatchUrl');
case "GEOIP":
return t('rulesMatchCountry');
}
}
@@ -461,8 +491,8 @@ export default function ResourceRules(props: {
cell: ({ row }) => (
<Select
defaultValue={row.original.match}
onValueChange={(value: "CIDR" | "IP" | "PATH") =>
updateRule(row.original.ruleId, { match: value })
onValueChange={(value: "CIDR" | "IP" | "PATH" | "GEOIP") =>
updateRule(row.original.ruleId, { match: value, value: value === "GEOIP" ? "US" : row.original.value })
}
>
<SelectTrigger className="min-w-[125px]">
@@ -472,6 +502,9 @@ export default function ResourceRules(props: {
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="GEOIP">{RuleMatch.GEOIP}</SelectItem>
)}
</SelectContent>
</Select>
)
@@ -480,15 +513,61 @@ export default function ResourceRules(props: {
accessorKey: "value",
header: t('value'),
cell: ({ row }) => (
<Input
defaultValue={row.original.value}
className="min-w-[200px]"
onBlur={(e) =>
updateRule(row.original.ruleId, {
value: e.target.value
})
}
/>
row.original.match === "GEOIP" ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="min-w-[200px] justify-between"
>
{row.original.value
? COUNTRIES.find((country) => country.code === row.original.value)?.name +
" (" + row.original.value + ")"
: t('selectCountry')}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="min-w-[200px] p-0">
<Command>
<CommandInput placeholder={t('searchCountries')} />
<CommandList>
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<CommandGroup>
{COUNTRIES.map((country) => (
<CommandItem
key={country.code}
value={country.name}
onSelect={() => {
updateRule(row.original.ruleId, { value: country.code });
}}
>
<Check
className={`mr-2 h-4 w-4 ${
row.original.value === country.code
? "opacity-100"
: "opacity-0"
}`}
/>
{country.name} ({country.code})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
defaultValue={row.original.value}
className="min-w-[200px]"
onBlur={(e) =>
updateRule(row.original.ruleId, {
value: e.target.value
})
}
/>
)
)
},
{
@@ -650,9 +729,7 @@ export default function ResourceRules(props: {
<FormControl>
<Select
value={field.value}
onValueChange={
field.onChange
}
onValueChange={field.onChange}
>
<SelectTrigger className="w-full">
<SelectValue />
@@ -669,6 +746,11 @@ export default function ResourceRules(props: {
<SelectItem value="CIDR">
{RuleMatch.CIDR}
</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="GEOIP">
{RuleMatch.GEOIP}
</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
@@ -692,7 +774,55 @@ export default function ResourceRules(props: {
}
/>
<FormControl>
<Input {...field}/>
{addRuleForm.watch("match") === "GEOIP" ? (
<Popover open={openAddRuleCountrySelect} onOpenChange={setOpenAddRuleCountrySelect}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openAddRuleCountrySelect}
className="w-full justify-between"
>
{field.value
? COUNTRIES.find((country) => country.code === field.value)?.name +
" (" + field.value + ")"
: t('selectCountry')}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder={t('searchCountries')} />
<CommandList>
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<CommandGroup>
{COUNTRIES.map((country) => (
<CommandItem
key={country.code}
value={country.name}
onSelect={() => {
field.onChange(country.code);
setOpenAddRuleCountrySelect(false);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
field.value === country.code
? "opacity-100"
: "opacity-0"
}`}
/>
{country.name} ({country.code})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -340,7 +340,21 @@ export default function Page() {
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: 0 // Will be set when resource is created
resourceId: 0, // Will be set when resource is created
hcEnabled: false,
hcPath: null,
hcMethod: null,
hcInterval: null,
hcTimeout: null,
hcHeaders: null,
hcScheme: null,
hcHostname: null,
hcPort: null,
hcFollowRedirects: null,
hcHealth: "unknown",
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null
};
setTargets([...targets, newTarget]);
@@ -446,6 +460,18 @@ export default function Page() {
method: target.method,
enabled: target.enabled,
siteId: target.siteId,
hcEnabled: target.hcEnabled,
hcPath: target.hcPath || null,
hcMethod: target.hcMethod || null,
hcInterval: target.hcInterval || null,
hcTimeout: target.hcTimeout || null,
hcHeaders: target.hcHeaders || null,
hcScheme: target.hcScheme || null,
hcHostname: target.hcHostname || null,
hcPort: target.hcPort || null,
hcFollowRedirects:
target.hcFollowRedirects || null,
hcStatus: target.hcStatus || null,
path: target.path,
pathMatchType: target.pathMatchType,
rewritePath: target.rewritePath,