Compare commits

...

14 Commits

Author SHA1 Message Date
Eduard Gert
a04e3afccb Show "Never" when a user never logged in instead of a date (#335)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-16 12:15:32 +01:00
Eduard Gert
bca327e4cf Add better search for network-routes by group name (#336) 2024-02-16 12:15:14 +01:00
Maycon Santos
6c74506316 Add templates for bugs and for feature request (#333) 2024-02-14 13:43:27 +01:00
Eduard Gert
663d7ea58c Add check to call initial users only once in dev mode (#332)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-13 15:11:37 +01:00
Eduard Gert
b701783dca Update ephemeral_peers to ephemeral (#331)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-13 14:12:31 +01:00
Eduard Gert
fc9a9dfa3e Block application and show loading until users are fetched (#330)
* Add option to ignore errors

* Block application and show loading until users are fetched
2024-02-13 14:08:43 +01:00
Eduard Gert
093efc08b3 Fix an issue of creating duplicate groups in the access control and network routes modal when group does not exist (#328)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-12 14:12:57 +01:00
Eduard Gert
dfa41a48e3 Hide the user invite button for selfhosted users (#327) 2024-02-12 14:08:10 +01:00
Eduard Gert
2cf366a5f8 Fix access control to show the correct modal (#326)
* Rename Access Control "Rule" to Access Control "Policy"

* Show the correct modal for Access Control
2024-02-12 14:07:53 +01:00
Eduard Gert
f91788faef Fix iOS detection and modal scrolling on Safari mobile (#325)
* Add better iOS detection

* Fix scrolling for Safari browser
2024-02-12 14:07:31 +01:00
Eduard Gert
ec7bb76f1e Fix closing of tab when creating setup-key (#324) 2024-02-12 14:06:59 +01:00
Eduard Gert
15bab2cef4 Merge pull request #322
* Add unique key for nameservers
2024-02-12 14:05:26 +01:00
Eduard Gert
4fa3482c74 Merge pull request #318
* Fix redirect link to event streaming docs
2024-02-12 14:04:42 +01:00
Eduard Gert
f5059f485c Fix invalid token error message (#321) 2024-02-09 16:07:09 +01:00
25 changed files with 284 additions and 100 deletions

View File

@@ -0,0 +1,44 @@
---
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ['needs-triage']
assignees: ''
---
**Describe the problem**
A clear and concise description of what the problem is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Are you using NetBird Cloud?**
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
**NetBird version**
`netbird version`
**NetBird status -d output:**
If applicable, add the `netbird status -d' command output.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ['feature-request','needs-triage']
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -33,8 +33,8 @@ export default function AccessControlPage() {
</Breadcrumbs>
<h1>
{policies && policies.length > 1
? `${policies.length} Access Control Rules`
: "Access Control Rules"}
? `${policies.length} Access Control Policies`
: "Access Control Policies"}
</h1>
<Paragraph>
Create rules to manage access in your network and define what peers

View File

@@ -132,7 +132,7 @@ function PeerOverview() {
<Breadcrumbs.Item label={peer.ip} active />
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div className={"flex justify-between max-w-6xl items-start"}>
<div>
<div className={"flex items-center gap-3"}>
<h1 className={"flex items-center gap-3"}>

View File

@@ -252,6 +252,9 @@ function UserOverview({ user }: Props) {
function UserInformationCard({ user }: { user: User }) {
const isServiceUser = user.is_service_user || false;
const neverLoggedIn = dayjs(user.last_login).isBefore(
dayjs().subtract(1000, "years"),
);
return (
<Card>
@@ -307,10 +310,12 @@ function UserInformationCard({ user }: { user: User }) {
</>
}
value={
dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(user.last_login) +
")"
neverLoggedIn
? "Never"
: dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(user.last_login) +
")"
}
/>
</>

View File

@@ -31,8 +31,8 @@ const ModalOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/30 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-neutral-950/70",
"place-items-center overflow-y-auto",
"fixed top-0 left-0 bottom-0 right-0 grid z-50 bg-black/30 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-neutral-950/70",
"mx-auto place-items-start overflow-y-auto md:py-16",
className,
)}
{...props}
@@ -65,7 +65,7 @@ const ModalContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-0 md:top-[5%] z-50 grid w-full translate-x-[-50%] border border-neutral-200 bg-white py-6 dark:shadow-lg shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 ] sm:rounded-lg md:w-full dark:border-nb-gray-900 dark:bg-nb-gray",
"mx-auto relative top-0 z-50 grid w-full border border-neutral-200 bg-white py-6 dark:shadow-lg shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1 data-[state=open]:slide-in-from-left-1 sm:rounded-lg md:w-full dark:border-nb-gray-900 dark:bg-nb-gray",
className,
maxWidthClass,
)}

View File

@@ -1,8 +1,9 @@
import { useOidcUser } from "@axa-fr/react-oidc";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useApiCall } from "@utils/api";
import { useIsMd } from "@utils/responsive";
import { getLatestNetbirdRelease } from "@utils/version";
import React, { useContext, useEffect, useMemo, useState } from "react";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { User } from "@/interfaces/User";
import type { NetbirdRelease } from "@/interfaces/Version";
@@ -28,10 +29,18 @@ export default function ApplicationProvider({ children }: Props) {
const { oidcUser: user } = useOidcUser();
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const isMd = useIsMd();
const userRequest = useApiCall<User[]>("/users");
const userRequest = useApiCall<User[]>("/users", true);
const [show, setShow] = useState(false);
const requestCalled = useRef(false);
useEffect(() => {
userRequest.get().then();
if (!requestCalled.current) {
userRequest
.get()
.then(() => setShow(true))
.catch(() => setShow(true));
requestCalled.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -66,12 +75,14 @@ export default function ApplicationProvider({ children }: Props) {
setMobileNavOpen(!mobileNavOpen);
};
return (
return show ? (
<ApplicationContext.Provider
value={{ latestVersion, toggleMobileNav, latestUrl, mobileNavOpen, user }}
>
{children}
</ApplicationContext.Provider>
) : (
<FullScreenLoading />
);
}

View File

@@ -28,10 +28,10 @@ export default function PoliciesProvider({ children }: Props) {
message?: string,
) => {
notify({
title: "Access Control Rule " + policy.name,
title: "Access Control Policy " + policy.name,
description: message
? message
: "The access control rule was successfully updated",
: "The access control policy was successfully updated",
promise: request
.put(
{

View File

@@ -5,6 +5,10 @@ import { OperatingSystem } from "@/interfaces/OperatingSystem";
export default function useOperatingSystem() {
const isBrowser = typeof window !== "undefined";
const userAgent = isBrowser ? navigator.userAgent.toLowerCase() : "";
const iOS = isBrowser
? /(iP*)/g.test(navigator.userAgent) && navigator.maxTouchPoints > 2
: false;
if (iOS) return OperatingSystem.IOS;
return getOperatingSystem(userAgent);
}
@@ -12,7 +16,7 @@ export const getOperatingSystem = (os: string) => {
if (os.includes("darwin")) return OperatingSystem.APPLE as const;
if (os.includes("mac")) return OperatingSystem.APPLE as const;
if (os.includes("android")) return OperatingSystem.ANDROID as const;
if (os.includes("ios")) return OperatingSystem.APPLE as const;
if (os.includes("ios")) return OperatingSystem.IOS as const;
if (os.includes("win")) return OperatingSystem.WINDOWS as const;
return OperatingSystem.LINUX as const;
};

View File

@@ -12,6 +12,7 @@ export interface Route {
peer_groups?: string[];
routesGroups?: string[];
groupedRoutes?: GroupedRoute[];
group_names?: string[];
}
export interface GroupedRoute {
@@ -22,5 +23,6 @@ export interface GroupedRoute {
high_availability_count: number;
is_using_route_groups: boolean;
routes?: Route[];
group_names?: string[];
description?: string;
}

View File

@@ -112,9 +112,9 @@ export function AccessControlModalContent({
const firstRule = policy?.rules ? policy.rules[0] : undefined;
const [tab, setTab] = useState(() => {
if (!cell) return "rule";
if (!cell) return "policy";
if (cell == "name") return "general";
return "rule";
return "policy";
});
const [enabled, setEnabled] = useState<boolean>(policy?.enabled ?? true);
@@ -162,7 +162,9 @@ export function AccessControlModalContent({
const createOrUpdateGroups = uniqBy([...g1, ...g2], "name").map(
(g) => g.promise,
);
const groups = await Promise.all(createOrUpdateGroups);
const groups = await Promise.all(
createOrUpdateGroups.map((call) => call()),
);
let sources = sourceGroups
.map((g) => {
@@ -210,13 +212,13 @@ export function AccessControlModalContent({
mutate("/policies");
onSuccess && onSuccess(policy);
},
"The rule was successfully saved",
"The policy was successfully saved",
);
} else {
notify({
title: "Create Access Control Rule",
description: "Rule was created successfully.",
loadingMessage: "Creating your setup key...",
title: "Create Access Control Policy",
description: "Policy was created successfully.",
loadingMessage: "Creating your policy...",
promise: policyRequest.post(policyObj).then((policy) => {
mutate("/policies");
onSuccess && onSuccess(policy);
@@ -239,18 +241,20 @@ export function AccessControlModalContent({
icon={<AccessControlIcon className={"fill-netbird"} />}
title={
policy
? "Update Access Control Rule"
: "Create New Access Control Rule"
? "Update Access Control Policy"
: "Create New Access Control Policy"
}
description={
"Use this policy to restrict access to groups of resources."
}
description={"Use this rule to restrict access to groups of resources."}
color={"netbird"}
/>
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"rule"}>
<TabsTrigger value={"policy"}>
<ArrowRightLeft size={16} />
Rule
Policy
</TabsTrigger>
<TabsTrigger value={"general"}>
<Text
@@ -263,7 +267,7 @@ export function AccessControlModalContent({
</TabsTrigger>
</TabsList>
<TabsContent value={"rule"} className={"pb-8"}>
<TabsContent value={"policy"} className={"pb-8"}>
<div className={"px-8 flex-col flex gap-6"}>
<div className={"flex justify-between items-center"}>
<div>
@@ -357,10 +361,10 @@ export function AccessControlModalContent({
label={
<>
<Power size={15} />
Enable Rule
Enable Policy
</>
}
helpText={"Use this switch to enable or disable the rule."}
helpText={"Use this switch to enable or disable the policy."}
/>
</div>
</TabsContent>
@@ -369,7 +373,7 @@ export function AccessControlModalContent({
<div>
<Label>Name of the Rule</Label>
<HelpText>
Set an easily identifiable name for your rule.
Set an easily identifiable name for your policy.
</HelpText>
<Input
autoFocus={true}
@@ -382,7 +386,7 @@ export function AccessControlModalContent({
<div>
<Label>Description (optional)</Label>
<HelpText>
Write a short description to add more context to this rule.
Write a short description to add more context to this policy.
</HelpText>
<Textarea
value={description}
@@ -427,7 +431,7 @@ export function AccessControlModalContent({
) : (
<>
<PlusCircle size={16} />
Add Rule
Add Policy
</>
)}
</Button>

View File

@@ -18,12 +18,12 @@ export default function AccessControlActionCell({ policy }: Props) {
const deleteRule = async () => {
notify({
title: "Access Control Rule " + policy.name,
description: "The rule was successfully removed.",
title: "Access Control Policy " + policy.name,
description: "The policy was successfully removed.",
promise: policyRequest.del("", `/${policy.id}`).then(() => {
mutate("/policies");
}),
loadingMessage: "Deleting the rule...",
loadingMessage: "Deleting the policy...",
});
};
@@ -31,7 +31,7 @@ export default function AccessControlActionCell({ policy }: Props) {
const choice = await confirm({
title: `Delete '${policy.name}'?`,
description:
"Are you sure you want to delete this access control rule? This action cannot be undone.",
"Are you sure you want to delete this access control policy? This action cannot be undone.",
confirmText: "Delete",
cancelText: "Cancel",
type: "danger",

View File

@@ -26,7 +26,6 @@ import AccessControlNameCell from "@/modules/access-control/table/AccessControlN
import AccessControlPortsCell from "@/modules/access-control/table/AccessControlPortsCell";
import AccessControlProtocolCell from "@/modules/access-control/table/AccessControlProtocolCell";
import AccessControlSourcesCell from "@/modules/access-control/table/AccessControlSourcesCell";
import RouteModal from "@/modules/routes/RouteModal";
type Props = {
policies?: Policy[];
@@ -206,17 +205,17 @@ export default function AccessControlTable({ policies, isLoading }: Props) {
size={"large"}
/>
}
title={"Create New Rule"}
title={"Create New Policy"}
description={
"It looks like you don't have any rules yet. Rules can allow connections by specific protocol and ports."
"It looks like you don't have any policies yet. Policies can allow connections by specific protocol and ports."
}
button={
<RouteModal>
<AccessControlModal>
<Button variant={"primary"} className={""}>
<PlusCircle size={16} />
Add Rule
Add Policy
</Button>
</RouteModal>
</AccessControlModal>
}
learnMore={
<>
@@ -238,7 +237,7 @@ export default function AccessControlTable({ policies, isLoading }: Props) {
<AccessControlModal>
<Button variant={"primary"} className={"ml-auto"}>
<PlusCircle size={16} />
Add Rule
Add Policy
</Button>
</AccessControlModal>
)}

View File

@@ -2,19 +2,25 @@ import Button from "@components/Button";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import {Input} from "@components/Input";
import {Label} from "@components/Label";
import {Modal, ModalClose, ModalContent, ModalFooter, ModalTrigger,} from "@components/modal/Modal";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalTrigger,
} from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
import {notify} from "@components/Notification";
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import {PeerGroupSelector} from "@components/PeerGroupSelector";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@components/Tabs";
import {Textarea} from "@components/Textarea";
import {useApiCall} from "@utils/api";
import {cn, validator} from "@utils/helpers";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { Textarea } from "@components/Textarea";
import { useApiCall } from "@utils/api";
import { cn, validator } from "@utils/helpers";
import cidr from "ip-cidr";
import {uniqueId} from "lodash";
import { uniqueId } from "lodash";
import {
ExternalLinkIcon,
GlobeIcon,
@@ -26,10 +32,10 @@ import {
ServerIcon,
Text,
} from "lucide-react";
import React, {useEffect, useMemo, useReducer, useState} from "react";
import {useSWRConfig} from "swr";
import React, { useEffect, useMemo, useReducer, useState } from "react";
import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon";
import {Domain, Nameserver, NameserverGroup} from "@/interfaces/Nameserver";
import { Domain, Nameserver, NameserverGroup } from "@/interfaces/Nameserver";
import useGroupHelper from "@/modules/groups/useGroupHelper";
type Props = {
@@ -177,9 +183,10 @@ export function NameserverModalContent({
};
// Nameservers
const [nameservers, setNameservers] = useReducer(
nameServerReducer,
preset?.nameservers || [],
const [nameservers, setNameservers] = useReducer(nameServerReducer, [], () =>
preset?.nameservers
? preset.nameservers.map((ns) => ({ id: uniqueId("ns"), ...ns }))
: [],
);
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({

View File

@@ -1,4 +1,5 @@
import { useApiCall } from "@utils/api";
import { isEmpty } from "lodash";
import { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import { useGroups } from "@/contexts/GroupsProvider";
@@ -46,7 +47,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
return groupsToUpdate.map((group) => {
return {
name: group.name,
promise: updateOrCreateGroup(group),
promise: () => updateOrCreateGroup(group),
};
});
};
@@ -67,7 +68,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
return removePeerFromGroup(group);
});
return [...updateCalls, ...removeCalls] as Promise<Group>[];
return [...updateCalls.map((c) => c()), ...removeCalls] as Promise<Group>[];
};
const removePeerFromGroup = async (g: Group) => {
@@ -93,20 +94,24 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
const updateOrCreateGroup = async (selectedGroup: Group) => {
const groupPeers =
selectedGroup.peers &&
selectedGroup.peers.map((p) => {
const groupPeer = p as GroupPeer;
return groupPeer.id;
});
selectedGroup.peers
.map((p) => {
const groupPeer = p as GroupPeer;
return groupPeer.id;
})
.filter((p) => p !== undefined && p !== null);
// Update group if it has an id (only when peer prop is passed)
const hasId = !!selectedGroup.id;
const peers = isEmpty(groupPeers) ? undefined : groupPeers;
if (hasId) {
if (selectedGroup.name == "All" || !peer)
return Promise.resolve(selectedGroup);
return groupRequest.put(
{
name: selectedGroup.name,
peers: groupPeers || [],
peers: peers,
},
`/${selectedGroup.id}`,
);

View File

@@ -43,6 +43,12 @@ export const GroupedRouteTableColumns: ColumnDef<GroupedRoute>[] = [
accessorKey: "enabled",
sortingFn: "basic",
},
{
id: "group_names",
accessorFn: (row) => {
return row.group_names?.map((name) => name).join(", ");
},
},
{
accessorKey: "network",
header: ({ column }) => {
@@ -121,10 +127,11 @@ export default function NetworkRoutesTable({
setSorting={setSorting}
columns={GroupedRouteTableColumns}
data={groupedRoutes}
searchPlaceholder={"Search by network, range or name..."}
searchPlaceholder={"Search by network, range, name or groups..."}
columnVisibility={{
enabled: false,
description: false,
group_names: false,
}}
renderExpandedRow={(row) => {
const data = cloneDeep(row);

View File

@@ -41,6 +41,19 @@ export default function useGroupedRoutes({ routes }: Props) {
const countEnabledPeers = peerRoutes.filter((r) => r.enabled).length;
const allPeers = countPeersOfGroup + countEnabledPeers;
// Get the group names for better search results
const peerGroupNames =
groupPeerRoute?.peer_groups?.map((id) => {
return groups?.find((g) => g.id == id)?.name || "";
}) || [];
const routeGroups = routes.map((r) => r.groups).flat();
const distributionGroupNames = routeGroups.map((group) => {
return groups?.find((g) => g.id == group)?.name || "";
});
const allGroupNames = [...peerGroupNames, ...distributionGroupNames];
results.push({
id,
enabled: routes.find((r) => r.enabled) != undefined,
@@ -50,6 +63,7 @@ export default function useGroupedRoutes({ routes }: Props) {
is_using_route_groups: !!groupPeerRoute,
description: groupPeerRoute ? groupPeerRoute?.description : undefined,
routes: routes,
group_names: allGroupNames,
});
});
return results;

View File

@@ -113,7 +113,9 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
const createOrUpdateGroups = uniqBy([...g1, ...g2], "name").map(
(g) => g.promise,
);
const createdGroups = await Promise.all(createOrUpdateGroups);
const createdGroups = await Promise.all(
createOrUpdateGroups.map((call) => call()),
);
const peerGroups = routingPeerGroups
.map((g) => {
const find = createdGroups.find((group) => group.name === g.name);

View File

@@ -1,7 +1,8 @@
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { GroupedRoute, Route } from "@/interfaces/Route";
import RouteActionCell from "@/modules/routes/RouteActionCell";
import RouteActiveCell from "@/modules/routes/RouteActiveCell";
@@ -39,7 +40,6 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
),
cell: ({ row }) => <RouteActiveCell route={row.original} />,
},
{
id: "groups",
accessorFn: (r) => r.groups?.length,
@@ -50,6 +50,12 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
},
cell: ({ row }) => <RouteDistributionGroupsCell route={row.original} />,
},
{
id: "group_names",
accessorFn: (row) => {
return row.group_names?.map((name) => name).join(", ");
},
},
{
accessorKey: "id",
header: "",
@@ -58,6 +64,8 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
];
export default function RouteTable({ row }: Props) {
const { groups } = useGroups();
// Default sorting state of the table
const [sorting, setSorting] = useState<SortingState>([
{
@@ -74,6 +82,26 @@ export default function RouteTable({ row }: Props) {
const [currentRow, setCurrentRow] = useState<Route>();
const [currentCellClicked, setCurrentCellClicked] = useState("");
const data = useMemo(() => {
if (!row.routes) return [];
// Get the group names for better search results
return row.routes.map((route) => {
const distributionGroupNames =
route.groups?.map((id) => {
return groups?.find((g) => g.id === id)?.name || "";
}) || [];
const peerGroupNames =
route.peer_groups?.map((id) => {
return groups?.find((g) => g.id === id)?.name || "";
}) || [];
const allGroupNames = [...distributionGroupNames, ...peerGroupNames];
return {
...route,
group_names: allGroupNames,
} as Route;
});
}, [row.routes, groups]);
return (
<>
{editModal && currentRow && (
@@ -92,6 +120,9 @@ export default function RouteTable({ row }: Props) {
text={"Network Routes"}
manualPagination={true}
sorting={sorting}
columnVisibility={{
group_names: false,
}}
onRowClick={(row, cell) => {
setCurrentRow(row.original);
setEditModal(true);
@@ -99,7 +130,7 @@ export default function RouteTable({ row }: Props) {
}}
setSorting={setSorting}
columns={RouteTableColumns}
data={row.routes}
data={data}
/>
</>
);

View File

@@ -150,7 +150,9 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
const createOrUpdateGroups = uniqBy([...g1, ...g2], "name").map(
(g) => g.promise,
);
const createdGroups = await Promise.all(createOrUpdateGroups);
const createdGroups = await Promise.all(
createOrUpdateGroups.map((call) => call()),
);
const peerGroups = routingPeerGroups
.map((g) => {
const find = createdGroups.find((group) => group.name === g.name);

View File

@@ -27,7 +27,7 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
revoked: true,
auto_groups: setupKey.auto_groups,
usage_limit: setupKey.usage_limit,
ephemeral_peers: setupKey.ephemeral,
ephemeral: setupKey.ephemeral,
})
.then(() => {
mutate("/setup-keys");

View File

@@ -27,7 +27,7 @@ export default function SetupKeyGroupsCell({ setupKey }: Props) {
revoked: setupKey.revoked,
auto_groups: groups.map((group) => group.id),
usage_limit: setupKey.usage_limit,
ephemeral_peers: setupKey.ephemeral,
ephemeral: setupKey.ephemeral,
})
.then(() => {
setModal(false);

View File

@@ -47,13 +47,10 @@ export default function SetupKeyModal({ children }: Props) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key);
const { mutate } = useSWRConfig();
const handleSuccess = (setupKey: SetupKey) => {
setSetupKey(setupKey);
setSuccessModal(true);
mutate("/setup-keys").then();
mutate("/groups").then();
};
return (
@@ -131,6 +128,7 @@ type ModalProps = {
export function SetupKeyModalContent({ onSuccess }: ModalProps) {
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys");
const { mutate } = useSWRConfig();
const [name, setName] = useState("");
const [reusable, setReusable] = useState(false);
@@ -167,11 +165,12 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
revoked: false,
auto_groups: groups.map((group) => group.id),
usage_limit: reusable ? parseInt(usageLimit) : 1,
ephemeral_peers: ephemeralPeers,
ephemeral: ephemeralPeers,
})
.then((setupKey) => {
onSuccess && onSuccess(setupKey);
close && close();
mutate("/setup-keys");
mutate("/groups");
});
}),
loadingMessage: "Creating your setup key...",

View File

@@ -7,6 +7,7 @@ import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import GetStartedTest from "@components/ui/GetStartedTest";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import dayjs from "dayjs";
import { ExternalLinkIcon, MailPlus, PlusCircle } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
@@ -176,14 +177,16 @@ export default function UsersTable({ users, isLoading }: Props) {
}
rightSide={() => (
<>
{users && users?.length > 0 && (
<UserInviteModal>
<Button variant={"primary"} className={"ml-auto"}>
<MailPlus size={16} />
Invite User
</Button>
</UserInviteModal>
)}
{(isLocalDev() || isNetBirdHosted()) &&
users &&
users?.length > 0 && (
<UserInviteModal>
<Button variant={"primary"} className={"ml-auto"}>
<MailPlus size={16} />
Invite User
</Button>
</UserInviteModal>
)}
</>
)}
>

View File

@@ -4,7 +4,9 @@ import {
useOidcIdToken,
} from "@axa-fr/react-oidc";
import loadConfig from "@utils/config";
import { sleep } from "@utils/helpers";
import { usePathname } from "next/navigation";
import { isExpired } from "react-jwt";
import useSWR from "swr";
import { useErrorBoundary } from "@/contexts/ErrorBoundary";
@@ -36,19 +38,34 @@ async function apiRequest<T>(
return (await res.json()) as T;
}
export function useNetBirdFetch() {
export function useNetBirdFetch(ignoreError: boolean = false) {
const tokenSource = config.tokenSource || "accessToken";
const { idToken } = useOidcIdToken();
const { accessToken } = useOidcAccessToken();
const token = tokenSource.toLowerCase() == "idtoken" ? idToken : accessToken;
const handleErrors = useApiErrorHandling(ignoreError);
const isTokenExpired = async () => {
let attempts = 20;
while (isExpired(token) && attempts > 0) {
await sleep(500);
attempts = attempts - 1;
}
return isExpired(token);
};
const nativeFetch = async (input: RequestInfo, init?: RequestInit) => {
const token =
tokenSource.toLowerCase() == "idtoken" ? idToken : accessToken;
const tokenExpired = await isTokenExpired();
if (tokenExpired) {
return handleErrors({ code: 401, message: "token expired" });
}
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
};
return fetch(input, {
...init,
headers,
@@ -60,9 +77,9 @@ export function useNetBirdFetch() {
};
}
export default function useFetchApi<T>(url: string) {
const { fetch } = useNetBirdFetch();
const handleErrors = useApiErrorHandling();
export default function useFetchApi<T>(url: string, ignoreError = false) {
const { fetch } = useNetBirdFetch(ignoreError);
const handleErrors = useApiErrorHandling(ignoreError);
const { data, error, isLoading, isValidating, mutate } = useSWR(
url,
@@ -85,9 +102,9 @@ export default function useFetchApi<T>(url: string) {
} as const;
}
export function useApiCall<T>(url: string) {
const { fetch } = useNetBirdFetch();
const handleErrors = useApiErrorHandling();
export function useApiCall<T>(url: string, ignoreError = false) {
const { fetch } = useNetBirdFetch(ignoreError);
const handleErrors = useApiErrorHandling(ignoreError);
return {
post: async (data: any, suffix = "") => {
@@ -113,15 +130,23 @@ export function useApiCall<T>(url: string) {
};
}
export function useApiErrorHandling() {
export function useApiErrorHandling(ignoreError = false) {
const { login } = useOidc();
const currentPath = usePathname();
const { setError } = useErrorBoundary();
if (ignoreError)
return (err: ErrorResponse) => {
console.log(err);
return Promise.reject(err);
};
return (err: ErrorResponse) => {
if (err.code == 401 && err.message == "no valid authentication provided") {
return login(currentPath);
}
if (err.code == 401 && err.message == "token expired") {
return login(currentPath);
}
if (err.code == 401 && err.message == "token invalid") {
return setError(err);
}