Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DNS routes #390

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.287.0",
"lucide-react": "^0.383.0",
"next": "13.5.5",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
Expand Down
8 changes: 6 additions & 2 deletions src/app/(dashboard)/peer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import FullScreenLoading from "@components/ui/FullScreenLoading";
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import useRedirect from "@hooks/useRedirect";
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import dayjs from "dayjs";
Expand All @@ -33,7 +34,7 @@
Globe,
History,
LockIcon,
MapPin,

Check failure on line 37 in src/app/(dashboard)/peer/page.tsx

View workflow job for this annotation

GitHub Actions / Check

MapPin ==> mapping
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
Expand Down Expand Up @@ -66,8 +67,11 @@
export default function PeerPage() {
const queryParameter = useSearchParams();
const peerId = queryParameter.get("id");
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
return peer ? (
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);

useRedirect("/peers", false, !peerId);

return peer && !isLoading ? (
<PeerProvider peer={peer}>
<PeerOverview />
</PeerProvider>
Expand Down Expand Up @@ -375,7 +379,7 @@
copyText={"NetBird IP-Address"}
label={
<>
<MapPin size={16} />

Check failure on line 382 in src/app/(dashboard)/peer/page.tsx

View workflow job for this annotation

GitHub Actions / Check

MapPin ==> mapping
NetBird IP-Address
</>
}
Expand Down
3 changes: 3 additions & 0 deletions src/app/(dashboard)/team/user/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { generateColorFromString } from "@utils/helpers";
Expand Down Expand Up @@ -42,6 +43,8 @@ export default function UserPage() {
return users?.find((u) => u.id === userId);
}, [users, userId]);

useRedirect("/team/users", false, !userId);

return !isLoading && user ? (
<UserOverview user={user} />
) : (
Expand Down
2 changes: 1 addition & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ export default function NotFound() {
}

const Redirect = ({ url, queryParams }: Props) => {
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
useRedirect("/peers" + (queryParams && `?${queryParams}`));
return <FullScreenLoading />;
};
32 changes: 30 additions & 2 deletions src/components/NetworkRouteSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CommandItem } from "@components/Command";
import FullTooltip from "@components/FullTooltip";
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
import { IconArrowBack } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
Expand Down Expand Up @@ -62,8 +63,13 @@ export function NetworkRouteSelector({
const isSearching = search.length > 0;
const found =
dropdownOptions.filter((item) => {
const hasDomains = item?.domains ? item.domains.length > 0 : false;
const domains =
hasDomains && item?.domains ? item?.domains.join(" ") : "";
return (
item.network_id.includes(search) || item.network.includes(search)
item.network_id.includes(search) ||
item.network?.includes(search) ||
domains.includes(search)
);
}).length > 0;
return isSearching && !found;
Expand Down Expand Up @@ -117,6 +123,7 @@ export function NetworkRouteSelector({
>
{value.network}
</div>
<DomainList domains={value?.domains} />
</div>
) : (
<span>Select an existing network...</span>
Expand Down Expand Up @@ -208,7 +215,11 @@ export function NetworkRouteSelector({
return (
<CommandItem
key={option.network + option.network_id}
value={option.network + option.network_id}
value={
option.network +
option.network_id +
option?.domains?.join(", ")
}
onSelect={() => {
togglePeer(option);
setOpen(false);
Expand All @@ -226,6 +237,7 @@ export function NetworkRouteSelector({
>
{option.network}
</div>
<DomainList domains={option?.domains} />
</CommandItem>
);
})}
Expand All @@ -238,3 +250,19 @@ export function NetworkRouteSelector({
</Popover>
);
}

function DomainList({ domains }: { domains?: string[] }) {
const firstDomain = domains ? domains[0] : "";
return (
domains &&
domains.length > 0 && (
<FullTooltip
content={<div className={"text-xs max-w-sm"}>{domains.join(", ")}</div>}
>
<div className={"text-xs text-nb-gray-300"}>
{firstDomain} {domains.length > 1 && "+" + (domains.length - 1)}
</div>
</FullTooltip>
)
);
}
16 changes: 11 additions & 5 deletions src/components/modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface Props extends IconVariant {
className?: string;
margin?: string;
truncate?: boolean;
children?: React.ReactNode;
}
export default function ModalHeader({
icon,
Expand All @@ -19,18 +20,23 @@ export default function ModalHeader({
className = "pb-6 px-8",
margin = "mt-0",
truncate = false,
children,
}: Props) {
return (
<div className={cn(className, "min-w-0")}>
<div className={"flex items-start gap-5 pr-10 min-w-0"}>
{icon && <SquareIcon color={color} icon={icon} />}
<div className={"min-w-0"}>
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
<Paragraph
className={cn("text-sm", margin, truncate && "!block truncate")}
>
{description}
</Paragraph>
{children ? (
<>{children}</>
) : (
<Paragraph
className={cn("text-sm", margin, truncate && "!block truncate")}
>
{description}
</Paragraph>
)}
</div>
</div>
</div>
Expand Down
70 changes: 70 additions & 0 deletions src/components/ui/DomainListBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Badge from "@components/Badge";
import FullTooltip from "@components/FullTooltip";
import { GlobeIcon } from "lucide-react";
import * as React from "react";

type Props = {
domains: string[];
};
export const DomainListBadge = ({ domains }: Props) => {
const firstDomain = domains.length > 0 ? domains[0] : undefined;

return (
<DomainsTooltip domains={domains}>
<div className={"inline-flex items-center gap-2"}>
{firstDomain && (
<Badge variant={"gray"}>
<GlobeIcon size={10} />
{firstDomain}
</Badge>
)}
{domains && domains.length > 1 && (
<Badge variant={"gray"}>+ {domains.length - 1}</Badge>
)}
</div>
</DomainsTooltip>
);
};

export const DomainsTooltip = ({
domains,
children,
className,
}: {
domains: string[];
children: React.ReactNode;
className?: string;
}) => {
return (
<FullTooltip
interactive={false}
className={className}
content={
<div className={"flex flex-col gap-2 items-start"}>
{domains.map((domain) => {
return (
domain && (
<div
key={domain}
className={"flex gap-2 items-center justify-between w-full"}
>
<div
className={
"flex gap-2 items-center text-nb-gray-300 text-xs"
}
>
<GlobeIcon size={11} />
{domain}
</div>
</div>
)
);
})}
</div>
}
disabled={domains.length <= 1}
>
{children}
</FullTooltip>
);
};
88 changes: 88 additions & 0 deletions src/components/ui/InputDomain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Button from "@components/Button";
import { Input } from "@components/Input";
import { validator } from "@utils/helpers";
import { uniqueId } from "lodash";
import { GlobeIcon, MinusCircleIcon } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { Domain } from "@/interfaces/Domain";

type Props = {
value: Domain;
onChange: (d: Domain) => void;
onRemove: () => void;
onError?: (error: boolean) => void;
error?: string;
};
enum ActionType {
ADD = "ADD",
REMOVE = "REMOVE",
UPDATE = "UPDATE",
}

export const domainReducer = (state: Domain[], action: any): Domain[] => {
switch (action.type) {
case ActionType.ADD:
return [...state, { name: "", id: uniqueId("domain") }];
case ActionType.REMOVE:
return state.filter((_, i) => i !== action.index);
case ActionType.UPDATE:
return state.map((n, i) => (i === action.index ? action.d : n));
default:
return state;
}
};

export default function InputDomain({
value,
onChange,
onRemove,
onError,
}: Readonly<Props>) {
const [name, setName] = useState(value?.name || "");

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
onChange({ ...value, name: e.target.value });
};

const domainError = useMemo(() => {
if (name == "") {
return "";
}
const valid = validator.isValidDomain(name);
if (!valid) {
return "Please enter a valid domain, e.g. example.com or intra.example.com";
}
}, [name]);

useEffect(() => {
const hasError = domainError !== "" && domainError !== undefined;
onError?.(hasError);
return () => onError?.(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [domainError]);

return (
<div className={"flex gap-2 w-full"}>
<div className={"w-full"}>
<Input
customPrefix={<GlobeIcon size={15} />}
placeholder={"e.g., example.com"}
maxWidthClass={"w-full"}
value={name}
error={domainError}
onChange={handleNameChange}
/>
</div>

<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={onRemove}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
}
10 changes: 8 additions & 2 deletions src/contexts/RoutesProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export default function RoutesProvider({ children }: Props) {
onSuccess?: (route: Route) => void,
message?: string,
) => {
const hasDomains = route.domains ? route.domains.length > 0 : false;

notify({
title: "Network " + route.network_id + "-" + route.network,
description: message
Expand All @@ -48,7 +50,9 @@ export default function RoutesProvider({ children }: Props) {
peer: toUpdate.peer ?? (route.peer || undefined),
peer_groups:
toUpdate.peer_groups ?? (route.peer_groups || undefined),
network: route.network,
network: !hasDomains ? route.network : undefined,
domains: hasDomains ? route.domains : undefined,
keep_route: route.keep_route,
metric: toUpdate.metric ?? route.metric ?? 9999,
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
groups: toUpdate.groups ?? route.groups ?? [],
Expand Down Expand Up @@ -80,7 +84,9 @@ export default function RoutesProvider({ children }: Props) {
enabled: route.enabled,
peer: route.peer || undefined,
peer_groups: route.peer_groups || undefined,
network: route.network,
network: route?.network || undefined,
domains: route?.domains || undefined,
keep_route: route?.keep_route || false,
metric: route.metric || 9999,
masquerade: route.masquerade,
groups: route.groups || [],
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useOperatingSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const getOperatingSystem = (os: string) => {
if (os.toLowerCase().includes("android"))
return OperatingSystem.ANDROID as const;
if (os.toLowerCase().includes("ios")) return OperatingSystem.IOS as const;
if (os.toLowerCase().includes("ipad")) return OperatingSystem.IOS as const;
if (os.toLowerCase().includes("iphone")) return OperatingSystem.IOS as const;
if (os.toLowerCase().includes("windows"))
return OperatingSystem.WINDOWS as const;
return OperatingSystem.LINUX as const;
Expand Down
Loading
Loading