Skip to content

Commit b4b6d92

Browse files
authored
Add DNS routes (#390)
1 parent 4898742 commit b4b6d92

29 files changed

+754
-209
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"framer-motion": "^10.16.4",
5454
"ip-cidr": "^3.1.0",
5555
"lodash": "^4.17.21",
56-
"lucide-react": "^0.287.0",
56+
"lucide-react": "^0.383.0",
5757
"next": "13.5.5",
5858
"next-themes": "^0.2.1",
5959
"punycode": "^2.3.1",

src/app/(dashboard)/peer/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Separator from "@components/Separator";
2323
import FullScreenLoading from "@components/ui/FullScreenLoading";
2424
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
2525
import TextWithTooltip from "@components/ui/TextWithTooltip";
26+
import useRedirect from "@hooks/useRedirect";
2627
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
2728
import useFetchApi from "@utils/api";
2829
import dayjs from "dayjs";
@@ -66,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
6667
export default function PeerPage() {
6768
const queryParameter = useSearchParams();
6869
const peerId = queryParameter.get("id");
69-
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
70-
return peer ? (
70+
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
71+
72+
useRedirect("/peers", false, !peerId);
73+
74+
return peer && !isLoading ? (
7175
<PeerProvider peer={peer}>
7276
<PeerOverview />
7377
</PeerProvider>

src/app/(dashboard)/team/user/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
1010
import { PeerGroupSelector } from "@components/PeerGroupSelector";
1111
import Separator from "@components/Separator";
1212
import FullScreenLoading from "@components/ui/FullScreenLoading";
13+
import useRedirect from "@hooks/useRedirect";
1314
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
1415
import useFetchApi, { useApiCall } from "@utils/api";
1516
import { generateColorFromString } from "@utils/helpers";
@@ -42,6 +43,8 @@ export default function UserPage() {
4243
return users?.find((u) => u.id === userId);
4344
}, [users, userId]);
4445

46+
useRedirect("/team/users", false, !userId);
47+
4548
return !isLoading && user ? (
4649
<UserOverview user={user} />
4750
) : (

src/app/not-found.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ export default function NotFound() {
3535
}
3636

3737
const Redirect = ({ url, queryParams }: Props) => {
38-
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
38+
useRedirect("/peers" + (queryParams && `?${queryParams}`));
3939
return <FullScreenLoading />;
4040
};

src/components/NetworkRouteSelector.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CommandItem } from "@components/Command";
2+
import FullTooltip from "@components/FullTooltip";
23
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
34
import { IconArrowBack } from "@tabler/icons-react";
45
import useFetchApi from "@utils/api";
@@ -62,8 +63,13 @@ export function NetworkRouteSelector({
6263
const isSearching = search.length > 0;
6364
const found =
6465
dropdownOptions.filter((item) => {
66+
const hasDomains = item?.domains ? item.domains.length > 0 : false;
67+
const domains =
68+
hasDomains && item?.domains ? item?.domains.join(" ") : "";
6569
return (
66-
item.network_id.includes(search) || item.network.includes(search)
70+
item.network_id.includes(search) ||
71+
item.network?.includes(search) ||
72+
domains.includes(search)
6773
);
6874
}).length > 0;
6975
return isSearching && !found;
@@ -117,6 +123,7 @@ export function NetworkRouteSelector({
117123
>
118124
{value.network}
119125
</div>
126+
<DomainList domains={value?.domains} />
120127
</div>
121128
) : (
122129
<span>Select an existing network...</span>
@@ -208,7 +215,11 @@ export function NetworkRouteSelector({
208215
return (
209216
<CommandItem
210217
key={option.network + option.network_id}
211-
value={option.network + option.network_id}
218+
value={
219+
option.network +
220+
option.network_id +
221+
option?.domains?.join(", ")
222+
}
212223
onSelect={() => {
213224
togglePeer(option);
214225
setOpen(false);
@@ -226,6 +237,7 @@ export function NetworkRouteSelector({
226237
>
227238
{option.network}
228239
</div>
240+
<DomainList domains={option?.domains} />
229241
</CommandItem>
230242
);
231243
})}
@@ -238,3 +250,19 @@ export function NetworkRouteSelector({
238250
</Popover>
239251
);
240252
}
253+
254+
function DomainList({ domains }: { domains?: string[] }) {
255+
const firstDomain = domains ? domains[0] : "";
256+
return (
257+
domains &&
258+
domains.length > 0 && (
259+
<FullTooltip
260+
content={<div className={"text-xs max-w-sm"}>{domains.join(", ")}</div>}
261+
>
262+
<div className={"text-xs text-nb-gray-300"}>
263+
{firstDomain} {domains.length > 1 && "+" + (domains.length - 1)}
264+
</div>
265+
</FullTooltip>
266+
)
267+
);
268+
}

src/components/modal/ModalHeader.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface Props extends IconVariant {
1010
className?: string;
1111
margin?: string;
1212
truncate?: boolean;
13+
children?: React.ReactNode;
1314
}
1415
export default function ModalHeader({
1516
icon,
@@ -19,18 +20,23 @@ export default function ModalHeader({
1920
className = "pb-6 px-8",
2021
margin = "mt-0",
2122
truncate = false,
23+
children,
2224
}: Props) {
2325
return (
2426
<div className={cn(className, "min-w-0")}>
2527
<div className={"flex items-start gap-5 pr-10 min-w-0"}>
2628
{icon && <SquareIcon color={color} icon={icon} />}
2729
<div className={"min-w-0"}>
2830
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
29-
<Paragraph
30-
className={cn("text-sm", margin, truncate && "!block truncate")}
31-
>
32-
{description}
33-
</Paragraph>
31+
{children ? (
32+
<>{children}</>
33+
) : (
34+
<Paragraph
35+
className={cn("text-sm", margin, truncate && "!block truncate")}
36+
>
37+
{description}
38+
</Paragraph>
39+
)}
3440
</div>
3541
</div>
3642
</div>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Badge from "@components/Badge";
2+
import FullTooltip from "@components/FullTooltip";
3+
import { GlobeIcon } from "lucide-react";
4+
import * as React from "react";
5+
6+
type Props = {
7+
domains: string[];
8+
};
9+
export const DomainListBadge = ({ domains }: Props) => {
10+
const firstDomain = domains.length > 0 ? domains[0] : undefined;
11+
12+
return (
13+
<DomainsTooltip domains={domains}>
14+
<div className={"inline-flex items-center gap-2"}>
15+
{firstDomain && (
16+
<Badge variant={"gray"}>
17+
<GlobeIcon size={10} />
18+
{firstDomain}
19+
</Badge>
20+
)}
21+
{domains && domains.length > 1 && (
22+
<Badge variant={"gray"}>+ {domains.length - 1}</Badge>
23+
)}
24+
</div>
25+
</DomainsTooltip>
26+
);
27+
};
28+
29+
export const DomainsTooltip = ({
30+
domains,
31+
children,
32+
className,
33+
}: {
34+
domains: string[];
35+
children: React.ReactNode;
36+
className?: string;
37+
}) => {
38+
return (
39+
<FullTooltip
40+
interactive={false}
41+
className={className}
42+
content={
43+
<div className={"flex flex-col gap-2 items-start"}>
44+
{domains.map((domain) => {
45+
return (
46+
domain && (
47+
<div
48+
key={domain}
49+
className={"flex gap-2 items-center justify-between w-full"}
50+
>
51+
<div
52+
className={
53+
"flex gap-2 items-center text-nb-gray-300 text-xs"
54+
}
55+
>
56+
<GlobeIcon size={11} />
57+
{domain}
58+
</div>
59+
</div>
60+
)
61+
);
62+
})}
63+
</div>
64+
}
65+
disabled={domains.length <= 1}
66+
>
67+
{children}
68+
</FullTooltip>
69+
);
70+
};

src/components/ui/InputDomain.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Button from "@components/Button";
2+
import { Input } from "@components/Input";
3+
import { validator } from "@utils/helpers";
4+
import { uniqueId } from "lodash";
5+
import { GlobeIcon, MinusCircleIcon } from "lucide-react";
6+
import * as React from "react";
7+
import { useEffect, useMemo, useState } from "react";
8+
import { Domain } from "@/interfaces/Domain";
9+
10+
type Props = {
11+
value: Domain;
12+
onChange: (d: Domain) => void;
13+
onRemove: () => void;
14+
onError?: (error: boolean) => void;
15+
error?: string;
16+
};
17+
enum ActionType {
18+
ADD = "ADD",
19+
REMOVE = "REMOVE",
20+
UPDATE = "UPDATE",
21+
}
22+
23+
export const domainReducer = (state: Domain[], action: any): Domain[] => {
24+
switch (action.type) {
25+
case ActionType.ADD:
26+
return [...state, { name: "", id: uniqueId("domain") }];
27+
case ActionType.REMOVE:
28+
return state.filter((_, i) => i !== action.index);
29+
case ActionType.UPDATE:
30+
return state.map((n, i) => (i === action.index ? action.d : n));
31+
default:
32+
return state;
33+
}
34+
};
35+
36+
export default function InputDomain({
37+
value,
38+
onChange,
39+
onRemove,
40+
onError,
41+
}: Readonly<Props>) {
42+
const [name, setName] = useState(value?.name || "");
43+
44+
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45+
setName(e.target.value);
46+
onChange({ ...value, name: e.target.value });
47+
};
48+
49+
const domainError = useMemo(() => {
50+
if (name == "") {
51+
return "";
52+
}
53+
const valid = validator.isValidDomain(name);
54+
if (!valid) {
55+
return "Please enter a valid domain, e.g. example.com or intra.example.com";
56+
}
57+
}, [name]);
58+
59+
useEffect(() => {
60+
const hasError = domainError !== "" && domainError !== undefined;
61+
onError?.(hasError);
62+
return () => onError?.(false);
63+
// eslint-disable-next-line react-hooks/exhaustive-deps
64+
}, [domainError]);
65+
66+
return (
67+
<div className={"flex gap-2 w-full"}>
68+
<div className={"w-full"}>
69+
<Input
70+
customPrefix={<GlobeIcon size={15} />}
71+
placeholder={"e.g., example.com"}
72+
maxWidthClass={"w-full"}
73+
value={name}
74+
error={domainError}
75+
onChange={handleNameChange}
76+
/>
77+
</div>
78+
79+
<Button
80+
className={"h-[42px]"}
81+
variant={"default-outline"}
82+
onClick={onRemove}
83+
>
84+
<MinusCircleIcon size={15} />
85+
</Button>
86+
</div>
87+
);
88+
}

src/contexts/RoutesProvider.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export default function RoutesProvider({ children }: Props) {
3434
onSuccess?: (route: Route) => void,
3535
message?: string,
3636
) => {
37+
const hasDomains = route.domains ? route.domains.length > 0 : false;
38+
3739
notify({
3840
title: "Network " + route.network_id + "-" + route.network,
3941
description: message
@@ -48,7 +50,9 @@ export default function RoutesProvider({ children }: Props) {
4850
peer: toUpdate.peer ?? (route.peer || undefined),
4951
peer_groups:
5052
toUpdate.peer_groups ?? (route.peer_groups || undefined),
51-
network: route.network,
53+
network: !hasDomains ? route.network : undefined,
54+
domains: hasDomains ? route.domains : undefined,
55+
keep_route: route.keep_route,
5256
metric: toUpdate.metric ?? route.metric ?? 9999,
5357
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
5458
groups: toUpdate.groups ?? route.groups ?? [],
@@ -80,7 +84,9 @@ export default function RoutesProvider({ children }: Props) {
8084
enabled: route.enabled,
8185
peer: route.peer || undefined,
8286
peer_groups: route.peer_groups || undefined,
83-
network: route.network,
87+
network: route?.network || undefined,
88+
domains: route?.domains || undefined,
89+
keep_route: route?.keep_route || false,
8490
metric: route.metric || 9999,
8591
masquerade: route.masquerade,
8692
groups: route.groups || [],

src/hooks/useOperatingSystem.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const getOperatingSystem = (os: string) => {
1919
if (os.toLowerCase().includes("android"))
2020
return OperatingSystem.ANDROID as const;
2121
if (os.toLowerCase().includes("ios")) return OperatingSystem.IOS as const;
22+
if (os.toLowerCase().includes("ipad")) return OperatingSystem.IOS as const;
23+
if (os.toLowerCase().includes("iphone")) return OperatingSystem.IOS as const;
2224
if (os.toLowerCase().includes("windows"))
2325
return OperatingSystem.WINDOWS as const;
2426
return OperatingSystem.LINUX as const;

0 commit comments

Comments
 (0)