Merge branch 'dev' into refactor/save-button-positions

This commit is contained in:
Fred KISSIE
2025-12-10 20:04:59 +01:00
committed by GitHub
635 changed files with 19060 additions and 14065 deletions

View File

@@ -837,7 +837,9 @@ export default function ResourceAuthenticationPage() {
<Bot size="14" />
<span>
{authInfo.headerAuth
? t("resourceHeaderAuthProtectionEnabled")
? t(
"resourceHeaderAuthProtectionEnabled"
)
: t(
"resourceHeaderAuthProtectionDisabled"
)}
@@ -921,7 +923,8 @@ export default function ResourceAuthenticationPage() {
validateTag={(
tag
) => {
return z.email()
return z
.email()
.or(
z
.string()

View File

@@ -104,7 +104,7 @@ export default function GeneralForm() {
name: z.string().min(1).max(255),
niceId: z.string().min(1).max(255).optional(),
domainId: z.string().optional(),
proxyPort: z.int().min(1).max(65535).optional(),
proxyPort: z.int().min(1).max(65535).optional()
// enableProxy: z.boolean().optional()
})
.refine(
@@ -134,7 +134,7 @@ export default function GeneralForm() {
niceId: resource.niceId,
subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined,
proxyPort: resource.proxyPort || undefined
// enableProxy: resource.enableProxy || false
},
mode: "onChange"
@@ -168,7 +168,7 @@ export default function GeneralForm() {
const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain),
baseDomain: toUnicode(domain.baseDomain)
}));
setBaseDomains(domains);
setFormKey((key) => key + 1);
@@ -195,9 +195,11 @@ export default function GeneralForm() {
enabled: data.enabled,
name: data.name,
niceId: data.niceId,
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
subdomain: data.subdomain
? toASCII(data.subdomain)
: undefined,
domainId: data.domainId,
proxyPort: data.proxyPort,
proxyPort: data.proxyPort
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
@@ -223,7 +225,7 @@ export default function GeneralForm() {
niceId: data.niceId,
subdomain: data.subdomain,
fullDomain: resource.fullDomain,
proxyPort: data.proxyPort,
proxyPort: data.proxyPort
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
@@ -235,7 +237,9 @@ export default function GeneralForm() {
});
if (data.niceId && data.niceId !== resource?.niceId) {
router.replace(`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`);
router.replace(
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
);
} else {
router.refresh();
}
@@ -320,11 +324,15 @@ export default function GeneralForm() {
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("identifier")}</FormLabel>
<FormLabel>
{t("identifier")}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t("enterIdentifier")}
placeholder={t(
"enterIdentifier"
)}
className="flex-1"
/>
</FormControl>
@@ -360,10 +368,10 @@ export default function GeneralForm() {
.target
.value
? parseInt(
e
.target
.value
)
e
.target
.value
)
: undefined
)
}
@@ -498,17 +506,29 @@ export default function GeneralForm() {
<Button
onClick={() => {
if (selectedDomain) {
const sanitizedSubdomain = selectedDomain.subdomain
? finalizeSubdomainSanitize(selectedDomain.subdomain)
: "";
const sanitizedSubdomain =
selectedDomain.subdomain
? finalizeSubdomainSanitize(
selectedDomain.subdomain
)
: "";
const sanitizedFullDomain = sanitizedSubdomain
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
: selectedDomain.baseDomain;
const sanitizedFullDomain =
sanitizedSubdomain
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
: selectedDomain.baseDomain;
setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`);
form.setValue("domainId", selectedDomain.domainId);
form.setValue("subdomain", sanitizedSubdomain);
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
);
form.setValue(
"domainId",
selectedDomain.domainId
);
form.setValue(
"subdomain",
sanitizedSubdomain
);
setEditDomainOpen(false);
}

View File

@@ -14,7 +14,7 @@ import OrgProvider from "@app/providers/OrgProvider";
import { cache } from "react";
import ResourceInfoBox from "@app/components/ResourceInfoBox";
import { GetSiteResponse } from "@server/routers/site";
import { getTranslations } from 'next-intl/server';
import { getTranslations } from "next-intl/server";
interface ResourceLayoutProps {
children: React.ReactNode;
@@ -76,22 +76,22 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
const navItems = [
{
title: t('general'),
title: t("general"),
href: `/{orgId}/settings/resources/proxy/{niceId}/general`
},
{
title: t('proxy'),
title: t("proxy"),
href: `/{orgId}/settings/resources/proxy/{niceId}/proxy`
}
];
if (resource.http) {
navItems.push({
title: t('authentication'),
title: t("authentication"),
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
});
navItems.push({
title: t('rules'),
title: t("rules"),
href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
});
}
@@ -99,15 +99,12 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
return (
<>
<SettingsSectionTitle
title={t('resourceSetting', {resourceName: resource?.name})}
description={t('resourceSettingDescription')}
title={t("resourceSetting", { resourceName: resource?.name })}
description={t("resourceSettingDescription")}
/>
<OrgProvider org={org}>
<ResourceProvider
resource={resource}
authInfo={authInfo}
>
<ResourceProvider resource={resource} authInfo={authInfo}>
<div className="space-y-6">
<ResourceInfoBox />
<HorizontalTabs items={navItems}>

View File

@@ -330,18 +330,17 @@ function ProxyResourceTargetsForm({
{row.original.siteType === "newt" ? (
<Button
variant="outline"
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
className="flex items-center gap-2 w-full text-left cursor-pointer"
onClick={() =>
openHealthCheckDialog(row.original)
}
>
<Badge variant={getStatusColor(status)}>
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Badge>
<Settings className="h-4 w-4" />
<div
className={`flex items-center gap-1 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
>
<Settings className="h-4 w-4" />
{getStatusText(status)}
</div>
</Button>
) : (
<span>-</span>
@@ -558,7 +557,7 @@ function ProxyResourceTargetsForm({
<Input
defaultValue={row.original.ip}
placeholder="IP / Hostname"
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();

View File

@@ -114,23 +114,25 @@ export default function ResourceRules(props: {
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
const [openCountrySelect, setOpenCountrySelect] = useState(false);
const [countrySelectValue, setCountrySelectValue] = useState("");
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
useState(false);
const router = useRouter();
const t = useTranslations();
const { env } = useEnvContext();
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
const isMaxmindAvailable =
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
const RuleAction = {
ACCEPT: t('alwaysAllow'),
DROP: t('alwaysDeny'),
PASS: t('passToAuth')
ACCEPT: t("alwaysAllow"),
DROP: t("alwaysDeny"),
PASS: t("passToAuth")
} as const;
const RuleMatch = {
PATH: t('path'),
PATH: t("path"),
IP: "IP",
CIDR: t('ipAddressRange'),
COUNTRY: t('country')
CIDR: t("ipAddressRange"),
COUNTRY: t("country")
} as const;
const addRuleForm = useForm({
@@ -155,10 +157,10 @@ export default function ResourceRules(props: {
console.error(err);
toast({
variant: "destructive",
title: t('rulesErrorFetch'),
title: t("rulesErrorFetch"),
description: formatAxiosError(
err,
t('rulesErrorFetchDescription')
t("rulesErrorFetchDescription")
)
});
} finally {
@@ -179,8 +181,8 @@ export default function ResourceRules(props: {
if (isDuplicate) {
toast({
variant: "destructive",
title: t('rulesErrorDuplicate'),
description: t('rulesErrorDuplicateDescription')
title: t("rulesErrorDuplicate"),
description: t("rulesErrorDuplicateDescription")
});
return;
}
@@ -188,8 +190,8 @@ export default function ResourceRules(props: {
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidIpAddressRange'),
description: t('rulesErrorInvalidIpAddressRangeDescription')
title: t("rulesErrorInvalidIpAddressRange"),
description: t("rulesErrorInvalidIpAddressRangeDescription")
});
setLoading(false);
return;
@@ -197,8 +199,8 @@ export default function ResourceRules(props: {
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidUrl'),
description: t('rulesErrorInvalidUrlDescription')
title: t("rulesErrorInvalidUrl"),
description: t("rulesErrorInvalidUrlDescription")
});
setLoading(false);
return;
@@ -206,17 +208,22 @@ export default function ResourceRules(props: {
if (data.match === "IP" && !isValidIP(data.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidIpAddress'),
description: t('rulesErrorInvalidIpAddressDescription')
title: t("rulesErrorInvalidIpAddress"),
description: t("rulesErrorInvalidIpAddressDescription")
});
setLoading(false);
return;
}
if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) {
if (
data.match === "COUNTRY" &&
!COUNTRIES.some((c) => c.code === data.value)
) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidCountry'),
description: t('rulesErrorInvalidCountryDescription') || "Invalid country code."
title: t("rulesErrorInvalidCountry"),
description:
t("rulesErrorInvalidCountryDescription") ||
"Invalid country code."
});
setLoading(false);
return;
@@ -265,13 +272,13 @@ export default function ResourceRules(props: {
function getValueHelpText(type: string) {
switch (type) {
case "CIDR":
return t('rulesMatchIpAddressRangeDescription');
return t("rulesMatchIpAddressRangeDescription");
case "IP":
return t('rulesMatchIpAddress');
return t("rulesMatchIpAddress");
case "PATH":
return t('rulesMatchUrl');
return t("rulesMatchUrl");
case "COUNTRY":
return t('rulesMatchCountry');
return t("rulesMatchCountry");
}
}
@@ -288,10 +295,10 @@ export default function ResourceRules(props: {
console.error(err);
toast({
variant: "destructive",
title: t('rulesErrorUpdate'),
title: t("rulesErrorUpdate"),
description: formatAxiosError(
err,
t('rulesErrorUpdateDescription')
t("rulesErrorUpdateDescription")
)
});
throw err;
@@ -314,8 +321,10 @@ export default function ResourceRules(props: {
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidIpAddressRange'),
description: t('rulesErrorInvalidIpAddressRangeDescription')
title: t("rulesErrorInvalidIpAddressRange"),
description: t(
"rulesErrorInvalidIpAddressRangeDescription"
)
});
setLoading(false);
return;
@@ -326,8 +335,8 @@ export default function ResourceRules(props: {
) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidUrl'),
description: t('rulesErrorInvalidUrlDescription')
title: t("rulesErrorInvalidUrl"),
description: t("rulesErrorInvalidUrlDescription")
});
setLoading(false);
return;
@@ -335,8 +344,8 @@ export default function ResourceRules(props: {
if (rule.match === "IP" && !isValidIP(rule.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidIpAddress'),
description: t('rulesErrorInvalidIpAddressDescription')
title: t("rulesErrorInvalidIpAddress"),
description: t("rulesErrorInvalidIpAddressDescription")
});
setLoading(false);
return;
@@ -345,8 +354,8 @@ export default function ResourceRules(props: {
if (rule.priority === undefined) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidPriority'),
description: t('rulesErrorInvalidPriorityDescription')
title: t("rulesErrorInvalidPriority"),
description: t("rulesErrorInvalidPriorityDescription")
});
setLoading(false);
return;
@@ -357,8 +366,8 @@ export default function ResourceRules(props: {
if (priorities.length !== new Set(priorities).size) {
toast({
variant: "destructive",
title: t('rulesErrorDuplicatePriority'),
description: t('rulesErrorDuplicatePriorityDescription')
title: t("rulesErrorDuplicatePriority"),
description: t("rulesErrorDuplicatePriorityDescription")
});
setLoading(false);
return;
@@ -397,8 +406,8 @@ export default function ResourceRules(props: {
}
toast({
title: t('ruleUpdated'),
description: t('ruleUpdatedDescription')
title: t("ruleUpdated"),
description: t("ruleUpdatedDescription")
});
setRulesToRemove([]);
@@ -407,10 +416,10 @@ export default function ResourceRules(props: {
console.error(err);
toast({
variant: "destructive",
title: t('ruleErrorUpdate'),
title: t("ruleErrorUpdate"),
description: formatAxiosError(
err,
t('ruleErrorUpdateDescription')
t("ruleErrorUpdateDescription")
)
});
}
@@ -428,7 +437,7 @@ export default function ResourceRules(props: {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('rulesPriority')}
{t("rulesPriority")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -440,15 +449,18 @@ export default function ResourceRules(props: {
type="number"
onClick={(e) => e.currentTarget.focus()}
onBlur={(e) => {
const parsed = z.int()
const parsed = z
.int()
.optional()
.safeParse(e.target.value);
if (!parsed.data) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidIpAddress'), // correct priority or IP?
description: t('rulesErrorInvalidPriorityDescription')
title: t("rulesErrorInvalidIpAddress"), // correct priority or IP?
description: t(
"rulesErrorInvalidPriorityDescription"
)
});
setLoading(false);
return;
@@ -463,7 +475,7 @@ export default function ResourceRules(props: {
},
{
accessorKey: "action",
header: () => (<span className="p-3">{t('rulesAction')}</span>),
header: () => <span className="p-3">{t("rulesAction")}</span>,
cell: ({ row }) => (
<Select
defaultValue={row.original.action}
@@ -486,12 +498,18 @@ export default function ResourceRules(props: {
},
{
accessorKey: "match",
header: () => (<span className="p-3">{t('rulesMatchType')}</span>),
header: () => <span className="p-3">{t("rulesMatchType")}</span>,
cell: ({ row }) => (
<Select
defaultValue={row.original.match}
onValueChange={(value: "CIDR" | "IP" | "PATH" | "COUNTRY") =>
updateRule(row.original.ruleId, { match: value, value: value === "COUNTRY" ? "US" : row.original.value })
onValueChange={(
value: "CIDR" | "IP" | "PATH" | "COUNTRY"
) =>
updateRule(row.original.ruleId, {
match: value,
value:
value === "COUNTRY" ? "US" : row.original.value
})
}
>
<SelectTrigger className="min-w-[125px]">
@@ -502,7 +520,9 @@ export default function ResourceRules(props: {
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="COUNTRY">{RuleMatch.COUNTRY}</SelectItem>
<SelectItem value="COUNTRY">
{RuleMatch.COUNTRY}
</SelectItem>
)}
</SelectContent>
</Select>
@@ -510,8 +530,8 @@ export default function ResourceRules(props: {
},
{
accessorKey: "value",
header: () => (<span className="p-3">{t('value')}</span>),
cell: ({ row }) => (
header: () => <span className="p-3">{t("value")}</span>,
cell: ({ row }) =>
row.original.match === "COUNTRY" ? (
<Popover>
<PopoverTrigger asChild>
@@ -521,29 +541,43 @@ export default function ResourceRules(props: {
className="min-w-[200px] justify-between"
>
{row.original.value
? COUNTRIES.find((country) => country.code === row.original.value)?.name +
" (" + row.original.value + ")"
: t('selectCountry')}
? 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')} />
<CommandInput
placeholder={t("searchCountries")}
/>
<CommandList>
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<CommandEmpty>
{t("noCountryFound")}
</CommandEmpty>
<CommandGroup>
{COUNTRIES.map((country) => (
<CommandItem
key={country.code}
value={country.name}
onSelect={() => {
updateRule(row.original.ruleId, { value: country.code });
updateRule(
row.original.ruleId,
{ value: country.code }
);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
row.original.value === country.code
row.original.value ===
country.code
? "opacity-100"
: "opacity-0"
}`}
@@ -567,11 +601,10 @@ export default function ResourceRules(props: {
}
/>
)
)
},
{
accessorKey: "enabled",
header: () => (<span className="p-3">{t('enabled')}</span>),
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
@@ -583,14 +616,14 @@ export default function ResourceRules(props: {
},
{
id: "actions",
header: () => (<span className="p-3">{t('actions')}</span>),
header: () => <span className="p-3">{t("actions")}</span>,
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<Button
variant="outline"
onClick={() => removeRule(row.original.ruleId)}
>
{t('delete')}
{t("delete")}
</Button>
</div>
)
@@ -664,10 +697,10 @@ export default function ResourceRules(props: {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('rulesResource')}
{t("rulesResource")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('rulesResourceDescription')}
{t("rulesResourceDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -675,7 +708,7 @@ export default function ResourceRules(props: {
<div className="flex items-center space-x-2">
<SwitchInput
id="rules-toggle"
label={t('rulesEnable')}
label={t("rulesEnable")}
defaultChecked={rulesEnabled}
onCheckedChange={(val) => setRulesEnabled(val)}
/>
@@ -692,7 +725,9 @@ export default function ResourceRules(props: {
name="action"
render={({ field }) => (
<FormItem>
<FormLabel>{t('rulesAction')}</FormLabel>
<FormLabel>
{t("rulesAction")}
</FormLabel>
<FormControl>
<Select
value={field.value}
@@ -705,13 +740,19 @@ export default function ResourceRules(props: {
</SelectTrigger>
<SelectContent>
<SelectItem value="ACCEPT">
{RuleAction.ACCEPT}
{
RuleAction.ACCEPT
}
</SelectItem>
<SelectItem value="DROP">
{RuleAction.DROP}
{
RuleAction.DROP
}
</SelectItem>
<SelectItem value="PASS">
{RuleAction.PASS}
{
RuleAction.PASS
}
</SelectItem>
</SelectContent>
</Select>
@@ -725,11 +766,15 @@ export default function ResourceRules(props: {
name="match"
render={({ field }) => (
<FormItem>
<FormLabel>{t('rulesMatchType')}</FormLabel>
<FormLabel>
{t("rulesMatchType")}
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
onValueChange={
field.onChange
}
>
<SelectTrigger className="w-full">
<SelectValue />
@@ -737,7 +782,9 @@ export default function ResourceRules(props: {
<SelectContent>
{resource.http && (
<SelectItem value="PATH">
{RuleMatch.PATH}
{
RuleMatch.PATH
}
</SelectItem>
)}
<SelectItem value="IP">
@@ -748,7 +795,9 @@ export default function ResourceRules(props: {
</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="COUNTRY">
{RuleMatch.COUNTRY}
{
RuleMatch.COUNTRY
}
</SelectItem>
)}
</SelectContent>
@@ -764,7 +813,7 @@ export default function ResourceRules(props: {
render={({ field }) => (
<FormItem className="gap-1">
<InfoPopup
text={t('value')}
text={t("value")}
info={
getValueHelpText(
addRuleForm.watch(
@@ -774,47 +823,100 @@ export default function ResourceRules(props: {
}
/>
<FormControl>
{addRuleForm.watch("match") === "COUNTRY" ? (
<Popover open={openAddRuleCountrySelect} onOpenChange={setOpenAddRuleCountrySelect}>
<PopoverTrigger asChild>
{addRuleForm.watch(
"match"
) === "COUNTRY" ? (
<Popover
open={
openAddRuleCountrySelect
}
onOpenChange={
setOpenAddRuleCountrySelect
}
>
<PopoverTrigger
asChild
>
<Button
variant="outline"
role="combobox"
aria-expanded={openAddRuleCountrySelect}
aria-expanded={
openAddRuleCountrySelect
}
className="w-full justify-between"
>
{field.value
? COUNTRIES.find((country) => country.code === field.value)?.name +
" (" + field.value + ")"
: t('selectCountry')}
? 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')} />
<CommandInput
placeholder={t(
"searchCountries"
)}
/>
<CommandList>
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<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>
))}
{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>
@@ -833,7 +935,7 @@ export default function ResourceRules(props: {
variant="outline"
disabled={!rulesEnabled}
>
{t('ruleSubmit')}
{t("ruleSubmit")}
</Button>
</div>
</form>
@@ -843,16 +945,22 @@ export default function ResourceRules(props: {
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const isActionsColumn = header.column.id === "actions";
const isActionsColumn =
header.column.id === "actions";
return (
<TableHead
key={header.id}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef
header.column
.columnDef
.header,
header.getContext()
)}
@@ -866,20 +974,30 @@ export default function ResourceRules(props: {
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => {
const isActionsColumn = cell.column.id === "actions";
return (
<TableCell
key={cell.id}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
{row
.getVisibleCells()
.map((cell) => {
const isActionsColumn =
cell.column.id ===
"actions";
return (
<TableCell
key={cell.id}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{flexRender(
cell.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (
@@ -888,7 +1006,7 @@ export default function ResourceRules(props: {
colSpan={columns.length}
className="h-24 text-center"
>
{t('rulesNoOne')}
{t("rulesNoOne")}
</TableCell>
</TableRow>
)}
@@ -907,7 +1025,7 @@ export default function ResourceRules(props: {
loading={loading}
disabled={loading}
>
{t('saveAllSettings')}
{t("saveAllSettings")}
</Button>
</div>
</SettingsContainer>

View File

@@ -190,8 +190,12 @@ const addTargetSchema = z
return false;
}
// If rewritePathType is provided, rewritePath must be provided
// Exception: stripPrefix can have an empty rewritePath (to just strip the prefix)
if (data.rewritePathType && !data.rewritePath) {
return false;
// Allow empty rewritePath for stripPrefix type
if (data.rewritePathType !== "stripPrefix") {
return false;
}
}
return true;
},
@@ -432,16 +436,16 @@ export default function Page() {
const newTarget: LocalTarget = {
...data,
path: isHttp ? (data.path || null) : null,
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
rewritePath: isHttp ? (data.rewritePath || null) : null,
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
path: isHttp ? data.path || null : null,
pathMatchType: isHttp ? data.pathMatchType || null : null,
rewritePath: isHttp ? data.rewritePath || null : null,
rewritePathType: isHttp ? data.rewritePathType || null : null,
siteType: site?.type || null,
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: 0, // Will be set when resource is created
priority: isHttp ? (data.priority || 100) : 100, // Default priority
priority: isHttp ? data.priority || 100 : 100, // Default priority
hcEnabled: false,
hcPath: null,
hcMethod: null,
@@ -507,7 +511,7 @@ export default function Page() {
try {
const payload = {
name: baseData.name,
http: baseData.http,
http: baseData.http
};
let sanitizedSubdomain: string | undefined;
@@ -577,7 +581,8 @@ export default function Page() {
hcFollowRedirects:
target.hcFollowRedirects || null,
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcUnhealthyInterval:
target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName
};
@@ -737,7 +742,7 @@ export default function Page() {
const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority",
header: () => (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 p-3">
{t("priority")}
<TooltipProvider>
<Tooltip>
@@ -780,7 +785,7 @@ export default function Page() {
const healthCheckColumn: ColumnDef<LocalTarget> = {
accessorKey: "healthCheck",
header: () => (<span className="p-3">{t("healthCheck")}</span>),
header: () => <span className="p-3">{t("healthCheck")}</span>,
cell: ({ row }) => {
const status = row.original.hcHealth || "unknown";
const isEnabled = row.original.hcEnabled;
@@ -826,18 +831,16 @@ export default function Page() {
{row.original.siteType === "newt" ? (
<Button
variant="outline"
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
className="flex items-center gap-2 w-full text-left cursor-pointer"
onClick={() =>
openHealthCheckDialog(row.original)
}
>
<Badge variant={getStatusColor(status)}>
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Badge>
<Settings className="h-4 w-4" />
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Button>
) : (
<span>-</span>
@@ -852,7 +855,7 @@ export default function Page() {
const matchPathColumn: ColumnDef<LocalTarget> = {
accessorKey: "path",
header: () => (<span className="p-3">{t("matchPath")}</span>),
header: () => <span className="p-3">{t("matchPath")}</span>,
cell: ({ row }) => {
const hasPathMatch = !!(
row.original.path || row.original.pathMatchType
@@ -914,7 +917,7 @@ export default function Page() {
const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address",
header: () => (<span className="p-3">{t("address")}</span>),
header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
@@ -1035,8 +1038,12 @@ export default function Page() {
{row.original.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
@@ -1050,7 +1057,7 @@ export default function Page() {
<Input
defaultValue={row.original.ip}
placeholder="IP / Hostname"
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();
@@ -1128,7 +1135,7 @@ export default function Page() {
const rewritePathColumn: ColumnDef<LocalTarget> = {
accessorKey: "rewritePath",
header: () => (<span className="p-3">{t("rewritePath")}</span>),
header: () => <span className="p-3">{t("rewritePath")}</span>,
cell: ({ row }) => {
const hasRewritePath = !!(
row.original.rewritePath || row.original.rewritePathType
@@ -1198,7 +1205,7 @@ export default function Page() {
const enabledColumn: ColumnDef<LocalTarget> = {
accessorKey: "enabled",
header: () => (<span className="p-3">{t("enabled")}</span>),
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<div className="flex items-center justify-center w-full">
<Switch
@@ -1219,7 +1226,7 @@ export default function Page() {
const actionsColumn: ColumnDef<LocalTarget> = {
id: "actions",
header: () => (<span className="p-3">{t("actions")}</span>),
header: () => <span className="p-3">{t("actions")}</span>,
cell: ({ row }) => (
<div className="flex items-center justify-end w-full">
<Button
@@ -1341,42 +1348,38 @@ export default function Page() {
</form>
</Form>
</SettingsSectionForm>
{resourceTypes.length > 1 && (
<>
<div className="mb-2">
<span className="text-sm font-medium">
{t("type")}
</span>
</div>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
// Update method default when switching resource type
addTargetForm.setValue(
"method",
value === "http"
? "http"
: null
);
}}
cols={2}
/>
</>
)}
</SettingsSectionBody>
</SettingsSection>
{resourceTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceType")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
// Update method default when switching resource type
addTargetForm.setValue(
"method",
value === "http"
? "http"
: null
);
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{baseForm.watch("http") ? (
<SettingsSection>
<SettingsSectionHeader>
@@ -1422,146 +1425,98 @@ export default function Page() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tcpUdpForm}>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); // block default enter refresh
}
}}
className="space-y-4"
id="tcp-udp-settings-form"
>
<Controller
control={
tcpUdpForm.control
}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"protocol"
)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
{...field}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"protocolSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
tcpUdpForm.control
}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"resourcePortNumber"
)}
</FormLabel>
<Form {...tcpUdpForm}>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); // block default enter refresh
}
}}
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
id="tcp-udp-settings-form"
>
<Controller
control={tcpUdpForm.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("protocol")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
{...field}
>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
{/* {build == "oss" && (
<FormField
control={
tcpUdpForm.control
}
name="enableProxy"
render={({
field
}) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
variant={
"outlinePrimarySquare"
}
checked={
field.value
}
onCheckedChange={
field.onChange
}
<SelectTrigger>
<SelectValue
placeholder={t(
"protocolSelect"
)}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{t(
"resourceEnableProxy"
)}
</FormLabel>
<FormDescription>
{t(
"resourceEnableProxyDescription"
)}
</FormDescription>
</div>
</FormItem>
)}
/>
)} */}
</form>
</Form>
</SettingsSectionForm>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={tcpUdpForm.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"resourcePortNumber"
)}
</FormLabel>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
)}
@@ -1596,13 +1551,21 @@ export default function Page() {
(
header
) => {
const isActionsColumn = header.column.id === "actions";
const isActionsColumn =
header
.column
.id ===
"actions";
return (
<TableHead
key={
header.id
}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{header.isPlaceholder
? null
@@ -1639,13 +1602,21 @@ export default function Page() {
(
cell
) => {
const isActionsColumn = cell.column.id === "actions";
const isActionsColumn =
cell
.column
.id ===
"actions";
return (
<TableCell
key={
cell.id
}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{flexRender(
cell
@@ -1711,7 +1682,7 @@ export default function Page() {
</div>
</>
) : (
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
<div className="text-center p-4">
<p className="text-muted-foreground mb-4">
{t("targetNoOne")}
</p>