Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a04e3afccb | ||
|
|
bca327e4cf | ||
|
|
6c74506316 | ||
|
|
663d7ea58c | ||
|
|
b701783dca | ||
|
|
fc9a9dfa3e | ||
|
|
093efc08b3 | ||
|
|
dfa41a48e3 | ||
|
|
2cf366a5f8 | ||
|
|
f91788faef | ||
|
|
ec7bb76f1e | ||
|
|
15bab2cef4 | ||
|
|
4fa3482c74 | ||
|
|
f5059f485c |
44
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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 />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user