Compare commits

..
26 changed files with 2202 additions and 1910 deletions

No files matched your search

+4 -4
View File
@@ -59,7 +59,7 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"lucide-react": "^0.562.0",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
@@ -6923,9 +6923,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.566.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.566.0.tgz",
"integrity": "sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==",
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+1 -1
View File
@@ -67,7 +67,7 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"lucide-react": "^0.562.0",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
+1 -1
View File
@@ -74,7 +74,7 @@ export const buttonVariants = cva(
"",
],
"danger-text": [
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50 rounded-sm",
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50",
],
"default-outline": [
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
+1 -1
View File
@@ -98,7 +98,7 @@ const SelectItem = React.forwardRef<
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<span className="flex-shrink-0">{icon}</span>
<div className="flex flex-col">
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+7 -25
View File
@@ -48,9 +48,6 @@ interface SelectDropdownProps {
children?: React.ReactNode;
maxHeight?: number;
triggerClassName?: string;
iconSize?: number;
truncate?: boolean;
compact?: boolean;
}
export function SelectDropdown({
@@ -71,9 +68,6 @@ export function SelectDropdown({
children,
maxHeight,
triggerClassName,
iconSize = 14,
truncate = false,
compact = false,
}: Readonly<SelectDropdownProps>) {
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
@@ -113,18 +107,15 @@ export function SelectDropdown({
const SelectedItem = () => {
return (
<div className={cn("flex items-center gap-2.5", truncate && "min-w-0")}>
{selected?.icon && <selected.icon size={iconSize} width={iconSize} />}
<div className={"flex items-center gap-2.5"}>
{selected?.icon && <selected.icon size={14} width={14} />}
<div
className={cn(
"flex flex-col text-sm font-medium",
size === "xs" && "text-xs",
truncate && "min-w-0",
)}
>
<span className={cn("text-nb-gray-200", truncate && "truncate")}>
{selected?.label}
</span>
<span className={"text-nb-gray-200"}>{selected?.label}</span>
</div>
</div>
);
@@ -225,22 +216,20 @@ export function SelectDropdown({
<ScrollArea
className={cn(
"overflow-y-auto flex flex-col gap-1",
compact ? "pl-1 pr-1" : "pl-2 pr-3",
!showSearch && (compact ? "pt-1" : "pt-2"),
"overflow-y-auto flex flex-col gap-1 pl-2 pr-3",
!showSearch && "pt-2",
)}
style={{
maxHeight: maxHeight ?? 380,
}}
>
<CommandGroup>
<div className={cn("grid grid-cols-1 gap-1 w-full", compact ? "pb-1" : "pb-2")}>
<div className={"grid grid-cols-1 gap-1 pb-2 w-full"}>
{filteredItems.map((option) => (
<SelectDropdownItem
option={option}
toggle={toggle}
key={option.value}
iconSize={iconSize}
showValue={showValues}
size={size}
/>
@@ -260,13 +249,11 @@ const SelectDropdownItem = ({
toggle,
showValue = false,
size = "sm",
iconSize = 14,
}: {
option: SelectOption;
toggle: (value: string) => void;
showValue?: boolean;
size: "xs" | "sm";
iconSize?: number;
}) => {
const value = option.value || "" + option.label || "";
const elementRef = useRef<HTMLDivElement>(null);
@@ -298,12 +285,7 @@ const SelectDropdownItem = ({
option?.disabled && "cursor-not-allowed",
)}
>
{option.icon && (
<div className={"shrink-0"}>
<option.icon size={iconSize} width={iconSize} />
</div>
)}
{option.icon && <option.icon size={14} width={14} />}
{option?.renderItem && option.renderItem()}
{!option?.renderItem && (
<div
-11
View File
@@ -53,7 +53,6 @@ declare module "@tanstack/table-core" {
}
interface SortingFns {
checkbox: SortingFn<unknown>;
datetime: SortingFn<unknown>;
}
}
@@ -100,15 +99,6 @@ const arrIncludesSomeExact: FilterFn<any> = (
return value.some((val) => val === rowValue);
};
const datetimeSort: SortingFn<any> = (rowA, rowB, columnId) => {
const aConnected = rowA.original?.connected;
const bConnected = rowB.original?.connected;
if (aConnected !== bConnected) return aConnected ? 1 : -1;
const a = dayjs(rowA.getValue(columnId)).valueOf();
const b = dayjs(rowB.getValue(columnId)).valueOf();
return a - b;
};
const checkboxSort: SortingFn<any> = (rowA, rowB, columnId) => {
const valueA =
columnId === "select" ? rowA.getIsSelected() : rowA.getValue(columnId);
@@ -334,7 +324,6 @@ export function DataTable<TData, TValue>({
},
sortingFns: {
checkbox: checkboxSort,
datetime: datetimeSort,
},
getRowId: useRowId ? (row) => row.id : undefined,
onRowSelectionChange: setRowSelection,
+2 -9
View File
@@ -14,7 +14,6 @@ type Props = {
center?: boolean;
className?: string;
sorting?: boolean;
onSort?: () => void;
name?: string;
};
export default function DataTableHeader({
@@ -24,20 +23,14 @@ export default function DataTableHeader({
center,
className,
sorting = true,
onSort,
name,
}: Props) {
const serverPagination = useOptionalServerPagination();
const handleSort = () => {
if (onSort) {
onSort();
} else {
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
column.toggleSorting(direction === "desc");
}
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
column.toggleSorting(direction === "desc");
if (name && serverPagination?.setSort) {
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
serverPagination.setSort(name, direction);
}
};
+2 -8
View File
@@ -9,11 +9,8 @@ import { useCountries } from "@/contexts/CountryProvider";
type Props = {
value: string;
onChange: (value: string) => void;
iconSize?: number;
popoverWidth?: "auto" | "content" | number;
truncate?: boolean;
};
export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth, truncate }: Props) => {
export const CountrySelector = ({ value, onChange }: Props) => {
const { countries, isLoading } = useCountries();
const countryList = useMemo(() => {
@@ -25,7 +22,7 @@ export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth,
}) =>
createElement(RoundedFlag, {
country: country.country_code,
size: iconSize,
size: 20,
...props,
});
return {
@@ -45,10 +42,7 @@ export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth,
searchPlaceholder={"Search country..."}
value={value}
onChange={onChange}
iconSize={iconSize}
options={countryList || []}
popoverWidth={popoverWidth}
truncate={truncate}
/>
</div>
);
+2 -10
View File
@@ -13,11 +13,7 @@ const CountryContext = React.createContext(
countries: Country[] | undefined;
isLoading: boolean;
getRegionByPeer: (peer: Peer) => string;
getRegionText: (
country_code: string,
city_name: string,
subdivision_code?: string,
) => string;
getRegionText: (country_code: string, city_name: string, subdivision_code?: string) => string;
},
);
@@ -25,11 +21,7 @@ export default function CountryProvider({ children }: Props) {
const { isRestricted } = usePermissions();
const getRegionByPeer = (peer: Peer) => "Unknown";
const getRegionText = (
country_code: string,
city_name: string,
subdivision_code?: string,
) => "Unknown";
const getRegionText = (country_code: string, city_name: string, _subdivision_code?: string) => "Unknown";
return isRestricted ? (
<CountryContext.Provider
+7 -11
View File
@@ -22,13 +22,6 @@ export interface ReverseProxy {
meta?: ReverseProxyMeta;
}
export interface AccessRestrictions {
allowed_cidrs?: string[];
blocked_cidrs?: string[];
allowed_countries?: string[];
blocked_countries?: string[];
}
export interface ReverseProxyMeta {
created_at: string;
status: ReverseProxyStatus;
@@ -94,6 +87,13 @@ export interface HeaderAuthConfig {
value: string;
}
export interface AccessRestrictions {
allowed_cidrs?: string[];
blocked_cidrs?: string[];
allowed_countries?: string[];
blocked_countries?: string[];
}
export interface ReverseProxyDomain {
id: string;
domain: string;
@@ -101,7 +101,6 @@ export interface ReverseProxyDomain {
type: ReverseProxyDomainType;
target_cluster?: string;
supports_custom_ports?: boolean;
require_subdomain?: boolean;
}
export enum ReverseProxyDomainType {
@@ -198,8 +197,5 @@ export const REVERSE_PROXY_DOMAIN_VERIFICATION_LINK =
export const REVERSE_PROXY_EVENTS_DOCS_LINK =
"https://docs.netbird.io/manage/reverse-proxy/access-logs";
export const REVERSE_PROXY_ACCESS_CONTROL_DOCS_LINK =
"https://docs.netbird.io/manage/reverse-proxy";
export const REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK =
"https://docs.netbird.io/manage/reverse-proxy#troubleshooting";
+2 -3
View File
@@ -91,11 +91,10 @@ export function DNSZoneModalContent({
if (domain == "") return "";
const valid = validator.isValidDomain(domain, {
allowWildcard: false,
allowOnlyTld: true,
preventLeadingAndTrailingDots: true,
allowOnlyTld: false,
});
if (!valid) {
return "Please enter a valid domain, e.g. internal, company.internal or intra.example.com";
return "Please enter a valid domain, e.g. company.internal or intra.example.com";
}
}, [domain]);
+7 -13
View File
@@ -138,18 +138,8 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
},
{
accessorKey: "last_seen",
header: ({ column, table }) => {
return (
<DataTableHeader
column={column}
onSort={() => {
const desc = column.getIsSorted() === "desc";
table.setSorting([{ id: "last_seen", desc: !desc }]);
}}
>
Last seen
</DataTableHeader>
);
header: ({ column }) => {
return <DataTableHeader column={column}>Last seen</DataTableHeader>;
},
sortingFn: "datetime",
cell: ({ row }) => <PeerLastSeenCell peer={row.original} />,
@@ -236,13 +226,17 @@ export default function PeersTable({
"netbird-table-sort" + path,
[
{
id: "last_seen",
id: "connected",
desc: true,
},
{
id: "name",
desc: false,
},
{
id: "last_seen",
desc: true,
},
],
);
@@ -0,0 +1,227 @@
import Button from "@components/Button";
import HelpText from "@components/HelpText";
import { Label } from "@components/Label";
import { CountrySelector } from "@components/ui/CountrySelector";
import { cn } from "@utils/helpers";
import cidr from "ip-cidr";
import { Globe, MinusCircleIcon, PlusCircle, ShieldCheck } from "lucide-react";
import React, { useState } from "react";
import { AccessRestrictions } from "@/interfaces/ReverseProxy";
let nextRowId = 0;
type Props = {
value: AccessRestrictions;
onChange: (value: AccessRestrictions) => void;
};
type CidrRow = { id: number; value: string };
type RowListProps = {
values: string[];
onChange: (values: string[]) => void;
};
function CidrList({ values, onChange }: Readonly<RowListProps>) {
const [rows, setRows] = useState<CidrRow[]>(() =>
values.map((v) => ({ id: nextRowId++, value: v })),
);
const [errors, setErrors] = useState<Record<number, string>>({});
const addRow = () => {
const next = [...rows, { id: nextRowId++, value: "" }];
setRows(next);
onChange(next.map((r) => r.value));
};
const updateRow = (id: number, raw: string) => {
const next = rows.map((r) => (r.id === id ? { ...r, value: raw } : r));
setRows(next);
onChange(next.map((r) => r.value));
if (raw && !cidr.isValidCIDR(raw)) {
setErrors((prev) => ({ ...prev, [id]: "Invalid CIDR format" }));
} else {
setErrors((prev) => {
const copy = { ...prev };
delete copy[id];
return copy;
});
}
};
const removeRow = (id: number) => {
const next = rows.filter((r) => r.id !== id);
setRows(next);
onChange(next.map((r) => r.value));
setErrors((prev) => {
const copy = { ...prev };
delete copy[id];
return copy;
});
};
return (
<div className="flex flex-col gap-2">
{rows.map((row) => (
<div key={row.id}>
<div className="flex gap-2">
<input
type="text"
value={row.value}
onChange={(e) => updateRow(row.id, e.target.value.trim())}
placeholder="e.g. 10.0.0.0/8"
className={cn(
"flex-1 h-[42px] px-3 text-sm font-mono bg-nb-gray-900/40 rounded-md outline-none",
"border placeholder:text-neutral-400/70",
errors[row.id]
? "border-red-400"
: "border-nb-gray-700",
)}
/>
<Button
className="h-[42px]"
variant="default-outline"
onClick={() => removeRow(row.id)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
{errors[row.id] && (
<p className="text-xs text-red-400 mt-1">{errors[row.id]}</p>
)}
</div>
))}
<Button variant="dotted" size="sm" onClick={addRow}>
<PlusCircle size={16} />
Add CIDR
</Button>
</div>
);
}
type CountryRow = { id: number; value: string };
function CountryList({ values, onChange }: Readonly<RowListProps>) {
const [rows, setRows] = useState<CountryRow[]>(() =>
values.map((v) => ({ id: nextRowId++, value: v })),
);
const addCountry = () => {
const next = [...rows, { id: nextRowId++, value: "" }];
setRows(next);
onChange(next.map((r) => r.value));
};
const updateCountry = (id: number, code: string) => {
if (rows.some((r) => r.value === code)) return;
const next = rows.map((r) => (r.id === id ? { ...r, value: code } : r));
setRows(next);
onChange(next.map((r) => r.value));
};
const removeCountry = (id: number) => {
const next = rows.filter((r) => r.id !== id);
setRows(next);
onChange(next.map((r) => r.value));
};
return (
<div className="flex flex-col gap-2">
{rows.map((row) => (
<div key={row.id} className="flex gap-2">
<div className="flex-1">
<CountrySelector
value={row.value}
onChange={(v) => updateCountry(row.id, v)}
/>
</div>
<Button
className="h-[42px]"
variant="default-outline"
onClick={() => removeCountry(row.id)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
))}
<Button variant="dotted" size="sm" onClick={addCountry}>
<PlusCircle size={16} />
Add Country
</Button>
</div>
);
}
export default function AccessRestrictionsSection({
value,
onChange,
}: Readonly<Props>) {
const update = (patch: Partial<AccessRestrictions>) => {
onChange({ ...value, ...patch });
};
return (
<div className="px-8 flex-col flex gap-6">
<div>
<Label>
<ShieldCheck size={14} />
CIDR Allowlist
</Label>
<HelpText>
Only connections from these IP ranges will be allowed. All other
IPs will be blocked. Leave empty to allow all.
</HelpText>
<CidrList
values={value.allowed_cidrs ?? []}
onChange={(v) => update({ allowed_cidrs: v })}
/>
</div>
<div>
<Label>
<ShieldCheck size={14} />
CIDR Blocklist
</Label>
<HelpText>
Block connections from these IP ranges. Takes priority over the
allowlist.
</HelpText>
<CidrList
values={value.blocked_cidrs ?? []}
onChange={(v) => update({ blocked_cidrs: v })}
/>
</div>
<div>
<Label>
<Globe size={14} />
Country Allowlist
</Label>
<HelpText>
Only connections from these countries will be allowed. All other
countries will be blocked. Leave empty to allow all.
</HelpText>
<CountryList
values={value.allowed_countries ?? []}
onChange={(v) => update({ allowed_countries: v })}
/>
</div>
<div>
<Label>
<Globe size={14} />
Country Blocklist
</Label>
<HelpText>
Block connections from these countries. Takes priority over the
allowlist.
</HelpText>
<CountryList
values={value.blocked_countries ?? []}
onChange={(v) => update({ blocked_countries: v })}
/>
</div>
</div>
);
}
@@ -1,315 +0,0 @@
import { useEffect, useMemo, useReducer, useRef } from "react";
import { Label } from "@components/Label";
import HelpText from "@components/HelpText";
import Button from "@components/Button";
import { Input } from "@components/Input";
import cidr from "ip-cidr";
import {
FlagIcon,
MinusCircleIcon,
NetworkIcon,
PlusIcon,
ShieldCheckIcon,
ShieldXIcon,
WorkflowIcon,
} from "lucide-react";
import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import { CountrySelector } from "@/components/ui/CountrySelector";
import { AccessRestrictions } from "@/interfaces/ReverseProxy";
type AccessAction = "allow" | "block";
type AccessRuleType = "country" | "ip" | "cidr";
const ACTION_OPTIONS: SelectOption[] = [
{
label: "Allow Only",
value: "allow",
icon: (props) => <ShieldCheckIcon {...props} className="text-green-500" />,
},
{
label: "Block Only",
value: "block",
icon: (props) => <ShieldXIcon {...props} className="text-red-500" />,
},
];
const TYPE_OPTIONS: SelectOption[] = [
{
label: "Country",
value: "country",
icon: (props) => <FlagIcon {...props} />,
},
{
label: "IP Address",
value: "ip",
icon: (props) => <WorkflowIcon {...props} />,
},
{
label: "CIDR Block",
value: "cidr",
icon: (props) => <NetworkIcon {...props} />,
},
];
type AccessRule = {
id: string;
action: AccessAction;
type: AccessRuleType;
value: string;
};
type RulesAction =
| { type: "add" }
| { type: "remove"; id: string }
| {
type: "update";
id: string;
field: "action" | "type" | "value";
value: string;
};
const nextId = () => crypto.randomUUID();
function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] {
switch (action.type) {
case "add":
return [
...state,
{ id: nextId(), action: "allow", type: "country", value: "" },
];
case "remove":
return state.filter((r) => r.id !== action.id);
case "update":
return state.map((r) => {
if (r.id !== action.id) return r;
if (action.field === "type") {
return { ...r, type: action.value as AccessRuleType, value: "" };
}
return { ...r, [action.field]: action.value };
});
}
}
function restrictionsToRules(
restrictions: AccessRestrictions | undefined,
): AccessRule[] {
if (!restrictions) return [];
const rules: AccessRule[] = [];
restrictions.allowed_countries?.forEach((v) =>
rules.push({ id: nextId(), action: "allow", type: "country", value: v }),
);
restrictions.blocked_countries?.forEach((v) =>
rules.push({ id: nextId(), action: "block", type: "country", value: v }),
);
restrictions.allowed_cidrs?.forEach((v) => {
const isIp = v.endsWith("/32");
rules.push({ id: nextId(), action: "allow", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
});
restrictions.blocked_cidrs?.forEach((v) => {
const isIp = v.endsWith("/32");
rules.push({ id: nextId(), action: "block", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
});
return rules;
}
function rulesToRestrictions(
rules: AccessRule[],
): AccessRestrictions | undefined {
const allowed_countries: string[] = [];
const blocked_countries: string[] = [];
const allowed_cidrs: string[] = [];
const blocked_cidrs: string[] = [];
for (const rule of rules) {
if (!rule.value) continue;
if (rule.type === "country") {
if (rule.action === "allow") allowed_countries.push(rule.value);
else blocked_countries.push(rule.value);
} else {
const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}/32` : rule.value;
if (rule.action === "allow") allowed_cidrs.push(value);
else blocked_cidrs.push(value);
}
}
const hasAny =
allowed_countries.length > 0 ||
blocked_countries.length > 0 ||
allowed_cidrs.length > 0 ||
blocked_cidrs.length > 0;
if (!hasAny) return undefined;
return {
...(allowed_countries.length > 0 && { allowed_countries }),
...(blocked_countries.length > 0 && { blocked_countries }),
...(allowed_cidrs.length > 0 && { allowed_cidrs }),
...(blocked_cidrs.length > 0 && { blocked_cidrs }),
};
}
type Props = {
value: AccessRestrictions | undefined;
onChange: (value: AccessRestrictions | undefined) => void;
onValidationChange?: (hasErrors: boolean) => void;
};
function validateRule(rule: AccessRule): string {
if (rule.type === "country" || !rule.value) return "";
if (rule.type === "ip") {
const val = rule.value.includes("/") ? rule.value : `${rule.value}/32`;
if (!cidr.isValidAddress(val)) {
return "Please enter a valid IP address, e.g., 85.203.15.42";
}
} else {
if (!rule.value.includes("/") || !cidr.isValidAddress(rule.value)) {
return "Please enter a valid CIDR block, e.g., 74.125.0.0/16";
}
}
return "";
}
export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange }: Props) => {
const [rules, dispatch] = useReducer(
rulesReducer,
value,
restrictionsToRules,
);
const errors = useMemo(
() => Object.fromEntries(rules.map((r) => [r.id, validateRule(r)])),
[rules],
);
const hasErrors = useMemo(
() => Object.values(errors).some((e) => e !== ""),
[errors],
);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const onValidationChangeRef = useRef(onValidationChange);
onValidationChangeRef.current = onValidationChange;
useEffect(() => {
onChangeRef.current(rulesToRestrictions(rules));
}, [rules]);
useEffect(() => {
onValidationChangeRef.current?.(hasErrors);
}, [hasErrors]);
return (
<div className={"flex-col flex"}>
<div>
<Label>Access Control Rules</Label>
<HelpText>
Define rules to allow or block traffic based on country, IP address,
or CIDR block.
<br />
Block rules always take priority over allow rules.
</HelpText>
</div>
{rules.length > 0 && (
<div className="flex flex-col gap-3 mt-1 mb-4">
{rules.map((rule) => (
<div key={rule.id} className="flex items-center">
<div className="w-[160px] shrink-0 [&_button]:rounded-r-none [&_button]:w-[160px]">
<SelectDropdown
value={rule.action}
onChange={(v) =>
dispatch({
type: "update",
id: rule.id,
field: "action",
value: v,
})
}
options={ACTION_OPTIONS}
compact
/>
</div>
<div className="w-[160px] shrink-0 -ml-px [&_button]:rounded-none [&_button]:w-[160px]">
<SelectDropdown
value={rule.type}
onChange={(v) =>
dispatch({
type: "update",
id: rule.id,
field: "type",
value: v,
})
}
options={TYPE_OPTIONS}
compact
/>
</div>
<div className="flex-1 min-w-0 -ml-px [&_button]:rounded-l-none [&_input]:rounded-l-none">
{rule.type === "country" ? (
<CountrySelector
iconSize={16}
popoverWidth={350}
truncate
value={rule.value}
onChange={(v) =>
dispatch({
type: "update",
id: rule.id,
field: "value",
value: v,
})
}
/>
) : (
<Input
placeholder={
rule.type === "ip"
? "e.g., 85.203.15.42"
: "e.g., 74.125.0.0/16"
}
value={rule.value}
onChange={(e) =>
dispatch({
type: "update",
id: rule.id,
field: "value",
value: e.target.value,
})
}
error={errors[rule.id]}
errorTooltip={true}
maxWidthClass="w-full"
/>
)}
</div>
<Button
variant="default-outline"
className="h-[42px] w-[42px] !px-0 shrink-0 ml-2"
onClick={() => dispatch({ type: "remove", id: rule.id })}
aria-label="Remove rule"
>
<MinusCircleIcon size={14} />
</Button>
</div>
))}
</div>
)}
<Button
variant="dotted"
className="w-full"
size="sm"
onClick={() => dispatch({ type: "add" })}
>
<PlusIcon size={14} />
Add Rule
</Button>
</div>
);
};
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -13,7 +13,6 @@ type Props = {
baseDomain: string;
onBaseDomainChange: (value: string) => void;
domainAlreadyExists: boolean;
subdomainRequired?: boolean;
clusterOffline?: {
clusterName: string;
};
@@ -25,16 +24,13 @@ export default function ReverseProxyDomainInput({
baseDomain,
onBaseDomainChange,
domainAlreadyExists,
subdomainRequired = false,
clusterOffline,
}: Readonly<Props>) {
return (
<div>
<Label>Domain</Label>
<HelpText>
{subdomainRequired
? "Enter a subdomain and select a domain for your service."
: "Optionally enter a subdomain, or use the domain directly."}
Enter a subdomain and select a domain for your service.
</HelpText>
<div className="flex items-start mt-2">
<div className="flex-1 min-w-0">
@@ -51,7 +47,7 @@ export default function ReverseProxyDomainInput({
? "This domain is already used by another service."
: undefined
}
placeholder={subdomainRequired ? "myapp" : "myapp (optional)"}
placeholder={"myapp"}
className="!rounded-r-none !border-r-0"
/>
</div>
@@ -23,13 +23,6 @@ function parseDomain(
.filter((d) => d.domain)
.sort((a, b) => b.domain.length - a.domain.length);
for (const d of sorted) {
if (fullDomain === d.domain) {
return {
subdomain: "",
baseDomain: d.domain,
isCustom: d.type === ReverseProxyDomainType.CUSTOM,
};
}
if (fullDomain.endsWith(`.${d.domain}`)) {
return {
subdomain: fullDomain.slice(0, -(d.domain.length + 1)),
@@ -110,11 +103,7 @@ export function useReverseProxyDomain({
return customDomain?.domain || freeDomain?.domain || "";
});
const fullDomain = baseDomain
? subdomain
? `${subdomain}.${baseDomain}`
: baseDomain
: subdomain;
const fullDomain = baseDomain ? `${subdomain}.${baseDomain}` : subdomain;
const domainAlreadyExists = useMemo(() => {
if (!reverseProxies || !fullDomain) return false;
@@ -1,14 +1,5 @@
import Badge from "@components/Badge";
import {
Binary,
FileCode2Icon,
Flag,
GlobeOff,
Mail,
Network,
RectangleEllipsis,
Users,
} from "lucide-react";
import { Binary, Globe, KeyRound, Mail, Network, RectangleEllipsis, ShieldOff, Users } from "lucide-react";
import * as React from "react";
import { ReverseProxyEvent } from "@/interfaces/ReverseProxy";
@@ -44,8 +35,8 @@ export const ReverseProxyEventsAuthMethodCell = ({ event }: Props) => {
};
case "header":
return {
icon: <FileCode2Icon size={12} />,
label: "HTTP Headers",
icon: <KeyRound size={12} />,
label: "Header Auth",
};
case "link":
case "magic_link":
@@ -61,12 +52,12 @@ export const ReverseProxyEventsAuthMethodCell = ({ event }: Props) => {
};
case "country_restricted":
return {
icon: <Flag size={12} />,
icon: <Globe size={12} />,
label: "Country Restricted",
};
case "geo_unavailable":
return {
icon: <GlobeOff size={12} />,
icon: <ShieldOff size={12} />,
label: "Geo Unavailable",
};
default:
@@ -19,17 +19,8 @@ export const ReverseProxyEventsLocationIpCell = ({ event }: Props) => {
const { getRegionText, isLoading } = useCountries();
const region = useMemo(() => {
return getRegionText(
event.country_code || "",
event.city_name || "",
event.subdivision_code,
);
}, [
getRegionText,
event.country_code,
event.city_name,
event.subdivision_code,
]);
return getRegionText(event.country_code || "", event.city_name || "", event.subdivision_code);
}, [getRegionText, event.country_code, event.city_name, event.subdivision_code]);
return (
<FullTooltip
Loaded 20 of 26 files, more files were not shown because too many files have changed in this diff. Show more