Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
650496f670 | ||
|
|
121778c4a6 | ||
|
|
d4102c5d04 | ||
|
|
e78c35bdbe | ||
|
|
6ebee98695 | ||
|
|
f4b28d5f40 | ||
|
|
b4b6d9295b | ||
|
|
4898742ee9 | ||
|
|
79164e9dd5 | ||
|
|
5caeab118b | ||
|
|
3f943bb7d4 | ||
|
|
96b939e6cc | ||
|
|
5e13548b81 | ||
|
|
2272a1d2a4 | ||
|
|
fc3da50346 | ||
|
|
6d4716cdad | ||
|
|
859916b1df | ||
|
|
80ce7d21b0 | ||
|
|
06fdbd8ec4 | ||
|
|
973cceff79 | ||
|
|
f4a2d6fae8 | ||
|
|
cb922b46b7 | ||
|
|
4c56ae704c | ||
|
|
fe6d8c9bd5 | ||
|
|
121976d101 | ||
|
|
f7071e00b6 | ||
|
|
6b73ccf102 | ||
|
|
87dcd00264 | ||
|
|
99f1bcc375 | ||
|
|
bf34c55110 | ||
|
|
1dfc6e2d75 | ||
|
|
b7860a8786 | ||
|
|
c9172e3a5f | ||
|
|
78d75134f9 | ||
|
|
071feb02f9 |
@@ -1,4 +1,3 @@
|
||||
# simple server configuration to replace nginx's default
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
@@ -7,10 +6,14 @@ server {
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
expires off;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
expires off;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_ENV: process.env.APP_ENV || "production",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@tabler/icons-react": "^2.39.0",
|
||||
"@tanstack/match-sorter-utils": "^8.8.4",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^18",
|
||||
@@ -35,6 +36,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^8",
|
||||
@@ -46,7 +48,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",
|
||||
@@ -1661,6 +1663,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.2.tgz",
|
||||
"integrity": "sha512-Q2Xn2/vQHRGLRXhQ5+BSLwhHkR3JVflxVKywH0Q6fVoAiUE8fFYL2pE5/l2ZiOiBDfA8qUqRnSxln4G/NFz1Sg=="
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz",
|
||||
@@ -2974,6 +2981,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/css-mediaquery": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
@@ -5339,9 +5351,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.287.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.287.0.tgz",
|
||||
"integrity": "sha512-auxP2bTGiMoELzX+6ItTeNzLmhGd/O+PHBsrXV2YwPXYCxarIFJhiMOSzFT9a1GWeYPSZtnWdLr79IVXr/5JqQ==",
|
||||
"version": "0.383.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.383.0.tgz",
|
||||
"integrity": "sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@tabler/icons-react": "^2.39.0",
|
||||
"@tanstack/match-sorter-utils": "^8.8.4",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^18",
|
||||
@@ -40,6 +41,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^8",
|
||||
@@ -51,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",
|
||||
|
||||
@@ -5,14 +5,12 @@ import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import ActivityTable from "@/modules/activity/ActivityTable";
|
||||
import { EventStreamingCard } from "@/modules/integrations/event-streaming/EventStreamingCard";
|
||||
|
||||
export default function Activity() {
|
||||
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
|
||||
@@ -50,7 +48,6 @@ export default function Activity() {
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={"Activity"}>
|
||||
{(isLocalDev() || isNetBirdHosted()) && <EventStreamingCard />}
|
||||
<ActivityTable events={events} isLoading={isLoading} />
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Integrations - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import { FileText, FingerprintIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import EventStreamingTab from "@/modules/integrations/event-streaming/EventStreamingTab";
|
||||
import IdentityProviderTab from "@/modules/integrations/idp-sync/IdentityProviderTab";
|
||||
|
||||
export default function Integrations() {
|
||||
const searchParams = useSearchParams();
|
||||
const currentTab = searchParams.get("tab");
|
||||
const [tab, setTab] = useState(currentTab || "event-streaming");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<VerticalTabs value={tab} onChange={setTab}>
|
||||
<VerticalTabs.List>
|
||||
<VerticalTabs.Trigger value="event-streaming">
|
||||
<FileText size={14} />
|
||||
Event Streaming
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="identity-provider">
|
||||
<FingerprintIcon size={14} />
|
||||
Identity Provider
|
||||
</VerticalTabs.Trigger>
|
||||
</VerticalTabs.List>
|
||||
<RestrictedAccess page={"Integrations"}>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
<EventStreamingTab />
|
||||
<IdentityProviderTab />
|
||||
</div>
|
||||
</RestrictedAccess>
|
||||
</VerticalTabs>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import Separator from "@components/Separator";
|
||||
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";
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
FlagIcon,
|
||||
Globe,
|
||||
History,
|
||||
LockIcon,
|
||||
MapPin,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
@@ -50,11 +52,14 @@ import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
|
||||
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
|
||||
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
@@ -62,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
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>
|
||||
@@ -124,6 +132,9 @@ function PeerOverview() {
|
||||
});
|
||||
};
|
||||
|
||||
const { isUser } = useLoggedInUser();
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
@@ -148,29 +159,31 @@ function PeerOverview() {
|
||||
/>
|
||||
<TextWithTooltip text={name} maxChars={30} />
|
||||
|
||||
<Modal
|
||||
open={showEditNameModal}
|
||||
onOpenChange={setShowEditNameModal}
|
||||
>
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<EditNameModal
|
||||
onSuccess={(newName) => {
|
||||
setName(newName);
|
||||
setShowEditNameModal(false);
|
||||
}}
|
||||
peer={peer}
|
||||
initialName={name}
|
||||
key={showEditNameModal ? 1 : 0}
|
||||
/>
|
||||
</Modal>
|
||||
{!isUser && (
|
||||
<Modal
|
||||
open={showEditNameModal}
|
||||
onOpenChange={setShowEditNameModal}
|
||||
>
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<EditNameModal
|
||||
onSuccess={(newName) => {
|
||||
setName(newName);
|
||||
setShowEditNameModal(false);
|
||||
}}
|
||||
peer={peer}
|
||||
initialName={name}
|
||||
key={showEditNameModal ? 1 : 0}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</h1>
|
||||
<LoginExpiredBadge loginExpired={peer.login_expired} />
|
||||
</div>
|
||||
@@ -192,7 +205,7 @@ function PeerOverview() {
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={() => updatePeer()}
|
||||
disabled={!hasChanges}
|
||||
disabled={!hasChanges || isUser}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
@@ -210,18 +223,32 @@ function PeerOverview() {
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>
|
||||
Login expiration is disabled for all peers added with an
|
||||
setup-key.
|
||||
</span>
|
||||
{!peer.user_id ? (
|
||||
<>
|
||||
<>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>
|
||||
Login expiration is disabled for all peers added
|
||||
with an setup-key.
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className={"w-full block"}
|
||||
disabled={!!peer.user_id}
|
||||
disabled={!!peer.user_id && !isUser}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
disabled={!peer.user_id}
|
||||
disabled={!peer.user_id || isUser}
|
||||
value={loginExpiration}
|
||||
onChange={setLoginExpiration}
|
||||
label={
|
||||
@@ -235,33 +262,74 @@ function PeerOverview() {
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
onChange={(set) =>
|
||||
!set
|
||||
? setSsh(false)
|
||||
: openSSHDialog().then((confirm) => setSsh(confirm))
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!isUser}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
disabled={isUser}
|
||||
onChange={(set) =>
|
||||
!set
|
||||
? setSsh(false)
|
||||
: openSSHDialog().then((confirm) => setSsh(confirm))
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
|
||||
<div>
|
||||
<Label>Assigned Groups</Label>
|
||||
<HelpText>
|
||||
Use groups to control what this peer can access.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
peer={peer}
|
||||
/>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!isUser}
|
||||
>
|
||||
<PeerGroupSelector
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
peer={peer}
|
||||
/>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +337,7 @@ function PeerOverview() {
|
||||
|
||||
<Separator />
|
||||
|
||||
{isLinux ? (
|
||||
{isLinux && !isUser ? (
|
||||
<div className={"px-8 py-6"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
@@ -281,7 +349,8 @@ function PeerOverview() {
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<div className={"gap-4 flex"}>
|
||||
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
|
||||
<AddRouteDropdownButton />
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,6 +375,8 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
<Card>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"NetBird IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
@@ -316,6 +387,8 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
@@ -325,6 +398,30 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Domain name"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
value={peer.dns_label}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
@@ -353,24 +450,6 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
value={peer.dns_label}
|
||||
/>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
|
||||
@@ -6,17 +6,39 @@ import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import React, { lazy, Suspense, useEffect } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function Peers() {
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{permission?.dashboard_view === "blocked" ? (
|
||||
<PeersBlockedView />
|
||||
) : (
|
||||
<PeersView />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersView() {
|
||||
const { data: peers, isLoading } = useFetchApi<Peer[]>("/peers");
|
||||
const { users } = useUsers();
|
||||
const { refresh } = useGroups();
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const peersWithUser = peers?.map((peer) => {
|
||||
if (!users) return peer;
|
||||
@@ -27,7 +49,7 @@ export default function Peers() {
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
@@ -56,6 +78,37 @@ export default function Peers() {
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable isLoading={isLoading} peers={peersWithUser} />
|
||||
</Suspense>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new device to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
After that you should be connected. If you have further questions
|
||||
check out our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"px-3 pt-1 pb-8 max-w-3xl w-full"}>
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,20 +2,35 @@
|
||||
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import { AlertOctagonIcon, FolderGit2Icon, ShieldIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
FolderGit2Icon,
|
||||
LockIcon,
|
||||
ShieldIcon,
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { useAccount } from "@/modules/account/useAccount";
|
||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||
import GroupsTab from "@/modules/settings/GroupsTab";
|
||||
import PermissionsTab from "@/modules/settings/PermissionsTab";
|
||||
|
||||
export default function NetBirdSettings() {
|
||||
const [tab, setTab] = useState("authentication");
|
||||
const queryParams = useSearchParams();
|
||||
const queryTab = queryParams.get("tab");
|
||||
const [tab, setTab] = useState(queryTab || "authentication");
|
||||
const { isOwner } = useLoggedInUser();
|
||||
const account = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
if (queryTab) {
|
||||
setTab(queryTab);
|
||||
}
|
||||
}, [queryTab]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<VerticalTabs value={tab} onChange={setTab}>
|
||||
@@ -28,6 +43,10 @@ export default function NetBirdSettings() {
|
||||
<FolderGit2Icon size={14} />
|
||||
Groups
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="permissions">
|
||||
<LockIcon size={14} />
|
||||
Permissions
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
||||
<AlertOctagonIcon size={14} />
|
||||
Danger zone
|
||||
@@ -36,6 +55,7 @@ export default function NetBirdSettings() {
|
||||
<RestrictedAccess page={"Settings"}>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{account && <PermissionsTab account={account} />}
|
||||
{account && <GroupsTab account={account} />}
|
||||
{account && <DangerZoneTab account={account} />}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
@@ -22,16 +22,21 @@ export default function SetupKeys() {
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { groups } = useGroups();
|
||||
|
||||
const setupKeysWithGroups = setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
}) as Group[] | undefined,
|
||||
};
|
||||
});
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
})
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
||||
@@ -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";
|
||||
@@ -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} />
|
||||
) : (
|
||||
@@ -57,7 +60,7 @@ function UserOverview({ user }: Props) {
|
||||
const router = useRouter();
|
||||
const userRequest = useApiCall<User>("/users");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { loggedInUser, isOwnerOrAdmin } = useLoggedInUser();
|
||||
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
||||
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
||||
|
||||
const initialGroups = user.auto_groups;
|
||||
@@ -104,6 +107,7 @@ function UserOverview({ user }: Props) {
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
disabled={isUser}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
|
||||
@@ -117,6 +121,7 @@ function UserOverview({ user }: Props) {
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={"Users"}
|
||||
disabled={isUser}
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
)}
|
||||
@@ -156,28 +161,30 @@ function UserOverview({ user }: Props) {
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex gap-4"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={"w-full"}
|
||||
onClick={() => {
|
||||
user.is_service_user
|
||||
? router.push("/team/service-users")
|
||||
: router.push("/team/users");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{!isUser && (
|
||||
<div className={"flex gap-4"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={"w-full"}
|
||||
onClick={() => {
|
||||
user.is_service_user
|
||||
? router.push("/team/service-users")
|
||||
: router.push("/team/users");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!hasChanges}
|
||||
onClick={save}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!hasChanges}
|
||||
onClick={save}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl"}>
|
||||
@@ -190,6 +197,7 @@ function UserOverview({ user }: Props) {
|
||||
Groups will be assigned to peers added by this user.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
/>
|
||||
|
||||
@@ -64,4 +64,8 @@ p {
|
||||
display: table;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stepper-bg-variant .step-circle {
|
||||
@apply !border-[#1d2024];
|
||||
}
|
||||
@@ -1,14 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
export default function NotFound() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push("/peers");
|
||||
});
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
return <FullScreenLoading />;
|
||||
useEffect(() => {
|
||||
setQueryParams(tempQueryParams);
|
||||
setTempQueryParams("");
|
||||
setMounted(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return mounted ? (
|
||||
<Redirect
|
||||
url={window?.location?.pathname || "/"}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
useRedirect("/peers");
|
||||
return <FullScreenLoading />;
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setQueryParams(tempQueryParams);
|
||||
setTempQueryParams("");
|
||||
setMounted(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return mounted ? (
|
||||
<Redirect
|
||||
url={window?.location?.pathname || "/"}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import deIcon from "@/assets/countries/de.svg";
|
||||
|
||||
export const CountryDERounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={deIcon}
|
||||
alt={"de"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import euIcon from "@/assets/countries/eu.svg";
|
||||
|
||||
export const CountryEURounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={euIcon}
|
||||
alt={"eu"}
|
||||
fill={true}
|
||||
className={"object-cover object-center shrink-0"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import jpIcon from "@/assets/countries/jp.svg";
|
||||
|
||||
export const CountryJPRounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={jpIcon}
|
||||
alt={"eu"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import usIcon from "@/assets/countries/us.svg";
|
||||
|
||||
export const CountryUSRounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={usIcon}
|
||||
alt={"us"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" viewBox="0 0 5 3">
|
||||
<desc>Flag of Germany</desc>
|
||||
<rect id="black_stripe" width="5" height="3" y="0" x="0" fill="#000"/>
|
||||
<rect id="red_stripe" width="5" height="2" y="1" x="0" fill="#D00"/>
|
||||
<rect id="gold_stripe" width="5" height="1" y="2" x="0" fill="#FFCE00"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 493 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 810 540"><defs><g id="d"><g id="b"><path id="a" d="M0 0v1h.5z" transform="rotate(18 3.157 -.5)"/><use xlink:href="#a" transform="scale(-1 1)"/></g><g id="c"><use xlink:href="#b" transform="rotate(72)"/><use xlink:href="#b" transform="rotate(144)"/></g><use xlink:href="#c" transform="scale(-1 1)"/></g></defs><path fill="#039" d="M0 0h810v540H0z"/><g fill="#fc0" transform="matrix(30 0 0 30 405 270)"><use xlink:href="#d" y="-6"/><use xlink:href="#d" y="6"/><g id="e"><use xlink:href="#d" x="-6"/><use xlink:href="#d" transform="rotate(-144 -2.344 -2.11)"/><use xlink:href="#d" transform="rotate(144 -2.11 -2.344)"/><use xlink:href="#d" transform="rotate(72 -4.663 -2.076)"/><use xlink:href="#d" transform="rotate(72 -5.076 .534)"/></g><use xlink:href="#e" transform="scale(-1 1)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 888 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
|
||||
<rect fill="#fff" height="600" width="900"/>
|
||||
<circle fill="#bc002d" cx="450" cy="300" r="180"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 166 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 741 B |
BIN
src/assets/fonts/Inter.ttf
Normal file
BIN
src/assets/fonts/Inter.ttf
Normal file
Binary file not shown.
@@ -16,6 +16,8 @@ export default function CircleIcon({
|
||||
return (
|
||||
<span
|
||||
style={{ width: size + "px", height: size + "px" }}
|
||||
data-cy="circle-icon"
|
||||
data-cy-status={active ? "active" : "inactive"}
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
active
|
||||
|
||||
@@ -5,7 +5,7 @@ export type IconProps = {
|
||||
};
|
||||
|
||||
export const defaultIconProps: IconProps = {
|
||||
size: 16,
|
||||
size: 15,
|
||||
className:
|
||||
"dark:fill-nb-gray-400 fill-gray-500 peer-data-[active=true]/icon:dark:fill-white peer-data-[active=true]/icon:fill-gray-900 shrink-0",
|
||||
autoHeight: false,
|
||||
|
||||
BIN
src/assets/integrations/okta.png
Normal file
BIN
src/assets/integrations/okta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
19
src/assets/netbird-full.svg
Normal file
19
src/assets/netbird-full.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="133" height="23" viewBox="0 0 133 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_0_3)">
|
||||
<path d="M46.9438 7.5013C48.1229 8.64688 48.7082 10.3025 48.7082 12.4683V21.6663H46.1411V12.8362C46.1411 11.2809 45.7481 10.0851 44.9704 9.26566C44.1928 8.43783 43.1308 8.0281 41.7846 8.0281C40.4383 8.0281 39.3345 8.45455 38.5234 9.30747C37.7123 10.1604 37.3109 11.4063 37.3109 13.0369V21.6663H34.7188V6.06305H37.3109V8.28732C37.821 7.49294 38.5234 6.87416 39.4014 6.43934C40.2878 6.00452 41.2578 5.78711 42.3197 5.78711C44.2179 5.78711 45.7565 6.36408 46.9355 7.50966L46.9438 7.5013Z" fill="#F2F2F2"/>
|
||||
<path d="M67.1048 14.8344H54.6288C54.7208 16.373 55.2476 17.5771 56.2092 18.4384C57.1708 19.2997 58.3331 19.7345 59.6961 19.7345C60.8166 19.7345 61.7531 19.4753 62.4973 18.9485C63.2499 18.4301 63.7767 17.7277 64.0777 16.858H66.8706C66.4525 18.3548 65.6163 19.5756 64.3621 20.5205C63.1078 21.4571 61.5525 21.9337 59.6878 21.9337C58.2077 21.9337 56.8865 21.5992 55.7159 20.9386C54.5452 20.278 53.6337 19.3331 52.9648 18.1039C52.2958 16.8831 51.9697 15.4616 51.9697 13.8477C51.9697 12.2339 52.2958 10.8207 52.9397 9.60825C53.5836 8.39578 54.495 7.45924 55.6573 6.80702C56.828 6.15479 58.1659 5.82031 59.6878 5.82031C61.2096 5.82031 62.4806 6.14643 63.6178 6.79029C64.7551 7.43416 65.6331 8.32052 66.2518 9.44938C66.8706 10.5782 67.18 11.8576 67.18 13.2791C67.18 13.7725 67.1549 14.2909 67.0964 14.8428L67.1048 14.8344ZM63.8603 10.1769C63.4255 9.4661 62.8318 8.92258 62.0793 8.55465C61.3267 8.18673 60.4989 8.00277 59.5874 8.00277C58.2746 8.00277 57.1625 8.42086 56.2427 9.25705C55.3228 10.0932 54.796 11.2472 54.6623 12.7356H64.5126C64.5126 11.7489 64.2952 10.896 63.8603 10.1852V10.1769Z" fill="#F2F2F2"/>
|
||||
<path d="M73.7695 8.20355V17.4016C73.7695 18.1626 73.9284 18.6977 74.2545 19.0071C74.5806 19.3165 75.1409 19.4754 75.9352 19.4754H77.8418V21.6662H75.5088C74.0622 21.6662 72.9835 21.3317 72.2644 20.6711C71.5452 20.0105 71.1857 18.9151 71.1857 17.3933V8.19519H69.1621V6.0629H71.1857V2.13281H73.7779V6.0629H77.8501V8.19519H73.7779L73.7695 8.20355Z" fill="#F2F2F2"/>
|
||||
<path d="M85.9022 6.68902C86.9307 6.10369 88.093 5.80266 89.4058 5.80266C90.8106 5.80266 92.0732 6.13714 93.1937 6.79773C94.3142 7.46668 95.2006 8.39485 95.8444 9.59896C96.4883 10.8031 96.8144 12.2079 96.8144 13.7966C96.8144 15.3854 96.4883 16.7818 95.8444 18.011C95.2006 19.2486 94.3142 20.2018 93.1854 20.8875C92.0565 21.5732 90.7939 21.916 89.4141 21.916C88.0344 21.916 86.8805 21.6234 85.8687 21.0297C84.8569 20.4443 84.0876 19.6918 83.5775 18.7803V21.6568H80.9854V0.601562H83.5775V8.97182C84.1127 8.04365 84.8904 7.28272 85.9105 6.69738L85.9022 6.68902ZM93.4529 10.7362C92.9763 9.86654 92.3408 9.19759 91.5297 8.74605C90.7186 8.29451 89.8322 8.06037 88.8706 8.06037C87.909 8.06037 87.0394 8.29451 86.2366 8.75441C85.4255 9.22268 84.7817 9.89163 84.2967 10.778C83.8117 11.6643 83.5692 12.6845 83.5692 13.8384C83.5692 14.9924 83.8117 16.046 84.2967 16.9323C84.7817 17.8187 85.4255 18.4877 86.2366 18.9559C87.0394 19.4242 87.9174 19.65 88.8706 19.65C89.8239 19.65 90.727 19.4158 91.5297 18.9559C92.3324 18.4877 92.9763 17.8187 93.4529 16.9323C93.9296 16.046 94.1637 15.0091 94.1637 13.8134C94.1637 12.6176 93.9296 11.6142 93.4529 10.7362Z" fill="#F2F2F2"/>
|
||||
<path d="M100.318 3.01864C99.9749 2.67581 99.8076 2.25771 99.8076 1.76436C99.8076 1.27101 99.9749 0.852913 100.318 0.510076C100.661 0.167238 101.079 0 101.572 0C102.065 0 102.45 0.167238 102.784 0.510076C103.119 0.852913 103.286 1.27101 103.286 1.76436C103.286 2.25771 103.119 2.67581 102.784 3.01864C102.45 3.36148 102.049 3.52872 101.572 3.52872C101.095 3.52872 100.661 3.36148 100.318 3.01864ZM102.826 6.06237V21.6657H100.234V6.06237H102.826Z" fill="#F2F2F2"/>
|
||||
<path d="M111.773 6.52155C112.617 6.0282 113.646 5.77734 114.867 5.77734V8.45315H114.181C111.28 8.45315 109.825 10.0252 109.825 13.1776V21.6649H107.232V6.06165H109.825V8.5953C110.276 7.70058 110.928 7.00654 111.773 6.51319V6.52155Z" fill="#F2F2F2"/>
|
||||
<path d="M117.861 9.60732C118.505 8.40321 119.391 7.46668 120.52 6.80609C121.649 6.1455 122.92 5.81102 124.325 5.81102C125.537 5.81102 126.666 6.09533 127.711 6.64721C128.757 7.20746 129.551 7.94331 130.103 8.85475V0.601562H132.72V21.6735H130.103V18.7385C129.593 19.6667 128.832 20.436 127.828 21.0297C126.825 21.6317 125.646 21.9244 124.3 21.9244C122.953 21.9244 121.657 21.5816 120.528 20.8959C119.4 20.2102 118.513 19.257 117.869 18.0194C117.226 16.7818 116.899 15.377 116.899 13.805C116.899 12.233 117.226 10.8114 117.869 9.60732H117.861ZM129.392 10.7613C128.915 9.89163 128.28 9.22268 127.469 8.75441C126.658 8.28614 125.771 8.06037 124.81 8.06037C123.848 8.06037 122.962 8.28614 122.159 8.74605C121.356 9.20595 120.729 9.86654 120.253 10.7362C119.776 11.6058 119.542 12.6343 119.542 13.8134C119.542 14.9924 119.776 16.046 120.253 16.9323C120.729 17.8187 121.365 18.4877 122.159 18.9559C122.953 19.4242 123.84 19.65 124.81 19.65C125.78 19.65 126.666 19.4158 127.469 18.9559C128.272 18.4877 128.915 17.8187 129.392 16.9323C129.869 16.046 130.103 15.0175 130.103 13.8384C130.103 12.6594 129.869 11.6393 129.392 10.7613Z" fill="#F2F2F2"/>
|
||||
<path d="M21.4651 0.568359C17.8193 0.902835 16.0047 3.00167 15.3191 4.06363L4.66602 22.5183H17.5182L30.1949 0.568359H21.4651Z" fill="#F68330"/>
|
||||
<path d="M17.5265 22.5187L0 3.9302C0 3.9302 19.8177 -1.39633 21.7493 15.2188L17.5265 22.5187Z" fill="#F68330"/>
|
||||
<path d="M14.9255 4.75055L9.54883 14.0657L17.5177 22.5196L21.7405 15.2029C21.0715 9.49174 18.287 6.37276 14.9255 4.74219" fill="#F35E32"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_0_3">
|
||||
<rect width="132.72" height="22.5186" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -2,7 +2,7 @@ import { useOidc, useOidcUser } from "@axa-fr/react-oidc";
|
||||
import Button from "@components/Button";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import loadConfig from "@utils/config";
|
||||
import { ArrowRightIcon, LogOut } from "lucide-react";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -55,7 +55,7 @@ export const OIDCError = () => {
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
className={"mt-5"}
|
||||
onClick={() => login("/", { client_id: config.clientId })}
|
||||
onClick={() => logout("/", { client_id: config.clientId })}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon size={16} />
|
||||
@@ -83,7 +83,6 @@ export const OIDCError = () => {
|
||||
onClick={() => logout("/", { client_id: config.clientId })}
|
||||
>
|
||||
Logout
|
||||
<LogOut size={16} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OidcConfiguration,
|
||||
} from "@axa-fr/react-oidc/dist/vanilla/oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import loadConfig, { buildExtras } from "@utils/config";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -43,6 +44,19 @@ export default function OIDCProvider({ children }: Props) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
const params = useSearchParams()?.toString();
|
||||
const [, setQueryParams] = useLocalStorage("netbird-query-params", params);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
params?.includes("tab") ||
|
||||
params?.includes("search") ||
|
||||
params?.includes("id")
|
||||
) {
|
||||
setQueryParams(params);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const withCustomHistory = () => {
|
||||
return {
|
||||
|
||||
@@ -15,13 +15,25 @@ type ItemProps = {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const Item = ({ href, label, icon, active }: ItemProps) => {
|
||||
export const Item = ({
|
||||
href,
|
||||
label,
|
||||
icon,
|
||||
active,
|
||||
disabled = false,
|
||||
}: ItemProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={"flex items-center gap-2 group"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 group",
|
||||
disabled && "pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
size={16}
|
||||
className={"text-nb-gray-400 group-first:hidden"}
|
||||
|
||||
@@ -5,14 +5,16 @@ import React, { forwardRef } from "react";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ButtonGroup({ children, disabled }: Props) {
|
||||
function ButtonGroup({ children, disabled, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border-[1px] dark:border-nb-gray-900 border-neutral-200 overflow-hidden flex items-center justify-center shrink-0 border-separate",
|
||||
disabled ? "opacity-100 !border-nb-gray-900/20" : "",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -21,7 +23,10 @@ function ButtonGroup({ children, disabled }: Props) {
|
||||
}
|
||||
|
||||
const ButtonGroupButton = forwardRef(
|
||||
({ ...props }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
(
|
||||
{ className, ...props }: ButtonProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -31,6 +36,7 @@ const ButtonGroupButton = forwardRef(
|
||||
className={cn(
|
||||
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[41px]",
|
||||
"!py-2.5 !px-4",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ type CardListItemProps = {
|
||||
value: React.ReactNode;
|
||||
className?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
};
|
||||
|
||||
@@ -38,6 +39,7 @@ function CardListItem({
|
||||
value,
|
||||
className,
|
||||
copy = false,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
}: CardListItemProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
@@ -56,7 +58,10 @@ function CardListItem({
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
copy && copyToClipBoard(`${label} has been copied to clipboard.`)
|
||||
copy &&
|
||||
copyToClipBoard(
|
||||
`${copyText ? copyText : label} has been copied to clipboard.`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{tooltip ? (
|
||||
@@ -64,7 +69,7 @@ function CardListItem({
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
{copy && <Copy size={13} />}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -12,14 +12,14 @@ export default function HelpText({
|
||||
className,
|
||||
}: Props) {
|
||||
return (
|
||||
<p
|
||||
<span
|
||||
className={cn(
|
||||
"text-[.8rem] dark:text-nb-gray-300",
|
||||
"text-[.8rem] dark:text-nb-gray-300 block font-light tracking-wide",
|
||||
margin && "mb-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface InputProps
|
||||
icon?: React.ReactNode;
|
||||
error?: string;
|
||||
errorTooltip?: boolean;
|
||||
errorTooltipPosition?: "top" | "top-right";
|
||||
}
|
||||
|
||||
const inputVariants = cva("", {
|
||||
@@ -49,6 +50,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
maxWidthClass = "",
|
||||
error,
|
||||
errorTooltip = false,
|
||||
errorTooltipPosition = "top",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -105,9 +107,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
</div>
|
||||
{error && errorTooltip && (
|
||||
<div
|
||||
className={
|
||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
|
||||
}
|
||||
className={cn(
|
||||
errorTooltipPosition == "top" &&
|
||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center",
|
||||
errorTooltipPosition == "top-right" &&
|
||||
"absolute -right-6 top-2 h-[0px] w-full flex items-center pr-3 justify-end",
|
||||
)}
|
||||
>
|
||||
<FullTooltip
|
||||
content={
|
||||
@@ -120,7 +125,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
align={"center"}
|
||||
align={errorTooltipPosition == "top" ? "center" : "end"}
|
||||
side={"top"}
|
||||
keepOpen={true}
|
||||
>
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -117,6 +123,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{value.network}
|
||||
</div>
|
||||
<DomainList domains={value?.domains} />
|
||||
</div>
|
||||
) : (
|
||||
<span>Select an existing network...</span>
|
||||
@@ -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);
|
||||
@@ -226,6 +237,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{option.network}
|
||||
</div>
|
||||
<DomainList domains={option?.domains} />
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import type { ErrorResponse } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import classNames from "classnames";
|
||||
@@ -88,7 +89,7 @@ export default function Notification<T>({
|
||||
{loading ? (
|
||||
<Loader2 size={14} className={"animate-spin"} />
|
||||
) : error ? (
|
||||
<XIcon size={14} />
|
||||
<IconCircleX size={24} />
|
||||
) : (
|
||||
icon || <CheckIcon size={14} />
|
||||
)}
|
||||
|
||||
@@ -86,8 +86,6 @@ export function PeerGroupSelector({
|
||||
}
|
||||
|
||||
if (max == 1) setOpen(false);
|
||||
|
||||
setSearch("");
|
||||
};
|
||||
|
||||
// Remove group from the groupOptions if it does not have an id
|
||||
@@ -145,6 +143,7 @@ export function PeerGroupSelector({
|
||||
"min-h-[46px] w-full relative items-center",
|
||||
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
|
||||
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer hover:dark:bg-nb-gray-900/50",
|
||||
"disabled:pointer-events-none disabled:opacity-30",
|
||||
)}
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
@@ -199,6 +198,7 @@ export function PeerGroupSelector({
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
data-cy={"group-search-input"}
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CommandItem } from "@components/Command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import SmallOSIcon from "@components/ui/SmallOSIcon";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
@@ -11,6 +10,7 @@ import { sortBy, trim, unionBy } from "lodash";
|
||||
import { ChevronsUpDown, MapPin, SearchIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FcLinux } from "react-icons/fc";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
@@ -45,7 +45,7 @@ export function PeerSelector({
|
||||
|
||||
// Filter out peers that are not linux
|
||||
options = options.filter((peer) => {
|
||||
return getOperatingSystem(peer.os) == OperatingSystem.LINUX;
|
||||
return getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
||||
});
|
||||
|
||||
// Filter out excluded peers
|
||||
@@ -56,7 +56,7 @@ export function PeerSelector({
|
||||
});
|
||||
}
|
||||
|
||||
setDropdownOptions(unionBy(options, dropdownOptions, "name"));
|
||||
setDropdownOptions(unionBy(options, dropdownOptions, "id"));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [peers]);
|
||||
|
||||
@@ -100,6 +100,12 @@ export function PeerSelector({
|
||||
}
|
||||
}, [open, dropdownOptions]);
|
||||
|
||||
const LinuxIcon = (
|
||||
<span className={"grayscale brightness-[100%] contrast-[40%]"}>
|
||||
<FcLinux className={"text-white text-lg min-w-[20px] brightness-150"} />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
@@ -115,7 +121,7 @@ export function PeerSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative items-center group",
|
||||
"min-h-[46px] w-full relative items-center group",
|
||||
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
|
||||
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer enabled:hover:dark:bg-nb-gray-900/50",
|
||||
"disabled:opacity-40 disabled:cursor-default",
|
||||
@@ -135,7 +141,7 @@ export function PeerSelector({
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
<SmallOSIcon os={value.os} />
|
||||
{LinuxIcon}
|
||||
<TextWithTooltip text={value.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
@@ -249,7 +255,7 @@ export function PeerSelector({
|
||||
}}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
<SmallOSIcon os={option.os} />
|
||||
{LinuxIcon}
|
||||
<TextWithTooltip text={option.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,12 +21,19 @@ function SegmentedTabs({ value, onChange, children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function List({ children }: { children: React.ReactNode }) {
|
||||
function List({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TabsList
|
||||
className={
|
||||
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900"
|
||||
}
|
||||
className={cn(
|
||||
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</TabsList>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function SidebarItem({
|
||||
<li className={"px-4 cursor-pointer"}>
|
||||
<button
|
||||
className={classNames(
|
||||
"rounded-lg text-base w-full ",
|
||||
"rounded-lg text-[.95rem] w-full ",
|
||||
"font-normal ",
|
||||
className,
|
||||
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",
|
||||
|
||||
@@ -15,6 +15,7 @@ const iconVariant = cva(
|
||||
green: "bg-green-950 border-green-500 text-green-500",
|
||||
purple: "bg-purple-950 border-purple-500 text-purple-500",
|
||||
indigo: "bg-indigo-950 border-indigo-500 text-indigo-500",
|
||||
yellow: "bg-yellow-950 border-yellow-400 text-yellow-400",
|
||||
},
|
||||
size: {
|
||||
small: "w-8 h-8",
|
||||
|
||||
@@ -36,7 +36,7 @@ const Step = ({ children, step, line = true, center = false }: StepProps) => {
|
||||
className={cn(
|
||||
"h-[34px] w-[34px] shrink-0 rounded-full flex items-center justify-center font-medium text-xs relative z-0 border-4 transition-all",
|
||||
"dark:bg-nb-gray-900 dark:text-nb-gray-400 dark:border-nb-gray dark:group-hover:bg-nb-gray-800",
|
||||
"bg-nb-gray-100 text-nb-gray-400 border-white group-hover:bg-nb-gray-200",
|
||||
"bg-nb-gray-100 text-nb-gray-400 border-white group-hover:bg-nb-gray-200 step-circle",
|
||||
)}
|
||||
>
|
||||
{step}
|
||||
|
||||
@@ -75,7 +75,10 @@ const ModalContent = React.forwardRef<
|
||||
<>
|
||||
{children}
|
||||
{showClose && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<DialogPrimitive.Close
|
||||
data-cy={"modal-close"}
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -9,6 +9,8 @@ interface Props extends IconVariant {
|
||||
description: string | React.ReactNode;
|
||||
className?: string;
|
||||
margin?: string;
|
||||
truncate?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export default function ModalHeader({
|
||||
icon,
|
||||
@@ -17,14 +19,24 @@ export default function ModalHeader({
|
||||
color = "netbird",
|
||||
className = "pb-6 px-8",
|
||||
margin = "mt-0",
|
||||
truncate = false,
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={"flex items-start gap-5 pr-10"}>
|
||||
<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>
|
||||
<div className={"min-w-0"}>
|
||||
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
|
||||
<Paragraph className={cn("text-sm", margin)}>{description}</Paragraph>
|
||||
{children ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Paragraph
|
||||
className={cn("text-sm", margin, truncate && "!block truncate")}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from "@components/Button";
|
||||
import { CommandItem } from "@components/Command";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { SelectDropdownSearchInput } from "@components/select/SelectDropdownSearchInput";
|
||||
@@ -31,6 +32,7 @@ interface SelectDropdownProps {
|
||||
popoverWidth?: "auto" | number;
|
||||
options: SelectOption[];
|
||||
showSearch?: boolean;
|
||||
showValues?: boolean;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
isLoading?: boolean;
|
||||
@@ -43,6 +45,7 @@ export function SelectDropdown({
|
||||
popoverWidth = "auto",
|
||||
options,
|
||||
showSearch = false,
|
||||
showValues = false,
|
||||
placeholder = "Select...",
|
||||
searchPlaceholder = "Search...",
|
||||
isLoading = false,
|
||||
@@ -186,6 +189,7 @@ export function SelectDropdown({
|
||||
option={option}
|
||||
toggle={toggle}
|
||||
key={option.value}
|
||||
showValue={showValues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -201,9 +205,11 @@ export function SelectDropdown({
|
||||
const SelectDropdownItem = ({
|
||||
option,
|
||||
toggle,
|
||||
showValue = false,
|
||||
}: {
|
||||
option: SelectOption;
|
||||
toggle: (value: string) => void;
|
||||
showValue?: boolean;
|
||||
}) => {
|
||||
const value = option.value || "" + option.label || "";
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
@@ -233,6 +239,13 @@ const SelectDropdownItem = ({
|
||||
<span className={"text-nb-gray-200"}>{option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
{showValue && (
|
||||
<div className={"flex items-center gap-2.5 p-1"}>
|
||||
<Paragraph className={cn("text-sm text-right")}>
|
||||
{option.value}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</CommandItem>
|
||||
) : (
|
||||
<div className={"h-[35px] py-1 px-2"}></div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import DataTableGlobalSearch from "@components/table/DataTableGlobalSearch";
|
||||
import { DataTablePagination } from "@components/table/DataTablePagination";
|
||||
import DataTableResetFilterButton from "@components/table/DataTableResetFilterButton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -54,11 +55,15 @@ declare module "@tanstack/table-core" {
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
const val = row.getValue(columnId);
|
||||
if (!val) return false;
|
||||
if (typeof val !== "string") return false;
|
||||
const lowerCaseValue = removeAllSpaces(trim(value.toLowerCase()));
|
||||
return val.toLowerCase().includes(lowerCaseValue);
|
||||
try {
|
||||
const val = row.getValue(columnId);
|
||||
if (!val) return false;
|
||||
if (typeof val !== "string") return false;
|
||||
const lowerCaseValue = removeAllSpaces(trim(value.toLowerCase()));
|
||||
return val.toLowerCase().includes(lowerCaseValue);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const exactMatch: FilterFn<any> = (row, columnId, value) => {
|
||||
@@ -223,6 +228,15 @@ export function DataTableContent<TData, TValue>({
|
||||
const TableDataUnstyledComponent = as === "table" ? "td" : "div";
|
||||
const TableRowUnstyledComponent = as === "table" ? "tr" : "div";
|
||||
|
||||
/**
|
||||
* Reset all filters, search & set pagination to first page
|
||||
*/
|
||||
const resetFilters = () => {
|
||||
table.setPageIndex(0);
|
||||
setColumnFilters([]);
|
||||
setGlobalSearch("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative table-fixed-scroll", className)}>
|
||||
{!minimal && (
|
||||
@@ -238,6 +252,7 @@ export function DataTableContent<TData, TValue>({
|
||||
placeholder={searchPlaceholder}
|
||||
/>
|
||||
{children && children(table)}
|
||||
<DataTableResetFilterButton onClick={resetFilters} table={table} />
|
||||
<div className={"flex gap-4 flex-wrap grow"}>
|
||||
<div className={"flex gap-4 flex-wrap"}></div>
|
||||
{rightSide && rightSide(table)}
|
||||
|
||||
55
src/components/table/DataTableResetFilterButton.tsx
Normal file
55
src/components/table/DataTableResetFilterButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import Button from "@components/Button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/Tooltip";
|
||||
import { Table } from "@tanstack/react-table";
|
||||
import { FilterX } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props<TData> {
|
||||
table: Table<TData>;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function DataTableResetFilterButton<TData>({
|
||||
table,
|
||||
onClick,
|
||||
}: Props<TData>) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isDisabled =
|
||||
table.getState().columnFilters.length <= 0 &&
|
||||
table.getState().globalFilter === "";
|
||||
|
||||
return !isDisabled ? (
|
||||
<Tooltip delayDuration={1}>
|
||||
<TooltipTrigger
|
||||
asChild={true}
|
||||
onMouseOver={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"secondary"}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FilterX size={16} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
sideOffset={10}
|
||||
className={"px-3 py-2"}
|
||||
onPointerDownOutside={(event) => {
|
||||
if (hovered) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<span className={"text-xs text-neutral-300"}>
|
||||
Reset Filters & Search
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
@@ -28,9 +28,10 @@ export function DataTableRowsPerPage<TData>({
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
data-cy={"rows-per-page"}
|
||||
className="w-[200px] justify-between"
|
||||
>
|
||||
<RowsIcon size={15} className={"text-nb-gray-300"} />
|
||||
<RowsIcon size={15} className={"text-nb-gray-300 shrink-0"} />
|
||||
<div>
|
||||
<span className={"text-white"}>
|
||||
{table.getState().pagination.pageSize}
|
||||
@@ -47,6 +48,7 @@ export function DataTableRowsPerPage<TData>({
|
||||
<CommandItem
|
||||
key={val}
|
||||
value={val.toString()}
|
||||
data-cy={`rows-per-page-value`}
|
||||
onSelect={(currentValue) => {
|
||||
table.setPageSize(Number(currentValue));
|
||||
setOpen(false);
|
||||
|
||||
92
src/components/ui/AnnouncementBanner.tsx
Normal file
92
src/components/ui/AnnouncementBanner.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { ArrowRightIcon, XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
|
||||
|
||||
const variants = cva(
|
||||
{},
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-nb-gray-900/50 border-nb-gray-800/30 border-b text-nb-gray-200",
|
||||
important: "from-netbird to-netbird-400 bg-gradient-to-b text-white",
|
||||
},
|
||||
tagBadge: {
|
||||
default: "bg-nb-gray-200/10 text-nb-gray-100 font-medium",
|
||||
important: "bg-white text-netbird font-medium",
|
||||
},
|
||||
closeButton: {
|
||||
default:
|
||||
"bg-nb-gray-900 rounded-md p-1 text-nb-gray-300 hover:bg-nb-gray-800",
|
||||
important:
|
||||
"bg-netbird-100 rounded-md p-1 text-netbird-600 hover:bg-white",
|
||||
},
|
||||
inlineLink: {
|
||||
default: "text-nb-blue-400 hover:underline",
|
||||
important: "!text-white underline hover:opacity-80",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AnnouncementVariant = VariantProps<typeof variants>;
|
||||
|
||||
export const AnnouncementBanner = () => {
|
||||
const { bannerHeight, closeAnnouncement, announcements } = useAnnouncement();
|
||||
const announcement = announcements?.find((a) => a.isOpen);
|
||||
|
||||
return announcement ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center text-sm px-8 font-light",
|
||||
variants({ variant: announcement.variant }),
|
||||
)}
|
||||
style={{ height: bannerHeight }}
|
||||
>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
{announcement.tag && (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-nb-gray-200/10 backdrop-blur text-nb-gray-100 font-medium tracking-wide uppercase text-[10px] py-2.5 px-2 rounded-md leading-[0]",
|
||||
variants({ tagBadge: announcement.variant }),
|
||||
)}
|
||||
>
|
||||
{announcement.tag}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{announcement.text}
|
||||
{announcement.link && (
|
||||
<InlineLink
|
||||
href={announcement.link || "#"}
|
||||
target={announcement.isExternal ? "_blank" : undefined}
|
||||
className={cn(
|
||||
"ml-2 !text-sm",
|
||||
variants({ inlineLink: announcement.variant }),
|
||||
)}
|
||||
>
|
||||
{announcement.linkText || "Learn more"}
|
||||
<ArrowRightIcon size={14} />
|
||||
</InlineLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{announcement.closeable && (
|
||||
<div className={"absolute right-0 px-4"}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md p-1 text-nb-gray-300 transition-all cursor-pointer",
|
||||
variants({ closeButton: announcement.variant }),
|
||||
)}
|
||||
onClick={() => closeAnnouncement(announcement.hash)}
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
70
src/components/ui/DomainListBadge.tsx
Normal file
70
src/components/ui/DomainListBadge.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -21,14 +21,14 @@ export default function GroupBadge({
|
||||
}: Props) {
|
||||
return (
|
||||
<Badge
|
||||
key={group.name}
|
||||
key={group.id}
|
||||
useHover={true}
|
||||
variant={"gray-ghost"}
|
||||
className={cn("transition-all group whitespace-nowrap", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
<TextWithTooltip text={group.name} maxChars={20} />
|
||||
<TextWithTooltip text={group?.name || ""} maxChars={20} />
|
||||
{children}
|
||||
{showX && (
|
||||
<XIcon
|
||||
|
||||
88
src/components/ui/InputDomain.tsx
Normal file
88
src/components/ui/InputDomain.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import * as React from "react";
|
||||
type Props = {
|
||||
data: {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string | React.ReactNode;
|
||||
noCopy?: boolean;
|
||||
tooltip?: boolean;
|
||||
}[];
|
||||
className?: string;
|
||||
};
|
||||
@@ -16,10 +18,11 @@ export const MinimalList = ({ data, className }: Props) => {
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<Card.ListItem
|
||||
copy
|
||||
copy={!item.noCopy}
|
||||
label={item.label}
|
||||
value={item.value}
|
||||
key={index}
|
||||
tooltip={item.tooltip !== false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
import { FcAndroidOs, FcLinux } from "react-icons/fc";
|
||||
import AppleLogo from "@/assets/os-icons/apple.svg";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
|
||||
export default function SmallOSIcon({ os }: { os: string }) {
|
||||
const icon = useMemo(() => {
|
||||
return getOperatingSystem(os.toLowerCase());
|
||||
}, [os]);
|
||||
|
||||
if (icon === OperatingSystem.WINDOWS)
|
||||
return <FaWindows className={"text-white text-md min-w-[20px]"} />;
|
||||
if (icon === OperatingSystem.APPLE)
|
||||
return (
|
||||
<div className={"min-w-[20px] flex items-center justify-center"}>
|
||||
<Image src={AppleLogo} alt={""} width={12} />
|
||||
</div>
|
||||
);
|
||||
if (icon === OperatingSystem.ANDROID)
|
||||
return <FcAndroidOs className={"text-white text-xl min-w-[20px]"} />;
|
||||
|
||||
return <FcLinux className={"text-white text-lg min-w-[20px]"} />;
|
||||
}
|
||||
@@ -24,14 +24,21 @@ export default function TextWithTooltip({
|
||||
<FullTooltip
|
||||
disabled={charCount <= maxChars || hideTooltip}
|
||||
interactive={false}
|
||||
className={"truncate w-full"}
|
||||
className={"truncate w-full min-w-0"}
|
||||
content={
|
||||
<div className={"max-w-xs break-all whitespace-normal"}>{text}</div>
|
||||
<div className={"max-w-xs break-all whitespace-normal text-xs"}>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={cn(className, "truncate")}>
|
||||
{charCount > maxChars ? text && `${text.slice(0, maxChars)}...` : text}
|
||||
</span>
|
||||
<div
|
||||
className={"w-full min-w-0 inline-block"}
|
||||
style={{
|
||||
maxWidth: `${maxChars - 2}ch`,
|
||||
}}
|
||||
>
|
||||
<div className={cn(className, "truncate")}>{text}</div>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function UserDropdown() {
|
||||
useHotkeys("shift+mod+l", () => logout(), []);
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
@@ -67,19 +68,23 @@ export default function UserDropdown() {
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
if (loggedInUser) {
|
||||
router.push(`/team/user?id=${loggedInUser.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<User2 size={14} />
|
||||
Profile Settings
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{permission.dashboard_view !== "blocked" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
if (loggedInUser) {
|
||||
router.push(`/team/user?id=${loggedInUser.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<User2 size={14} />
|
||||
Profile Settings
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem onClick={logoutSession}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<LogOutIcon size={14} />
|
||||
|
||||
@@ -19,6 +19,7 @@ const AnalyticsContext = React.createContext(
|
||||
{} as {
|
||||
initialized: boolean;
|
||||
trackPageView: () => void;
|
||||
trackEvent: (category: string, action: string, label: string) => void;
|
||||
},
|
||||
);
|
||||
const config = loadConfig();
|
||||
@@ -51,8 +52,20 @@ export default function AnalyticsProvider({ children }: Props) {
|
||||
ReactGA.send({ hitType: "pageview", page: path, title: document.title });
|
||||
};
|
||||
|
||||
const trackEvent = (category: string, action: string, label: string) => {
|
||||
if (isProduction() && ReactGA.isInitialized) {
|
||||
ReactGA.event({
|
||||
category: category,
|
||||
action: action,
|
||||
label: label,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnalyticsContext.Provider value={{ initialized, trackPageView }}>
|
||||
<AnalyticsContext.Provider
|
||||
value={{ initialized, trackPageView, trackEvent }}
|
||||
>
|
||||
{children}
|
||||
</AnalyticsContext.Provider>
|
||||
);
|
||||
|
||||
103
src/contexts/AnnouncementProvider.tsx
Normal file
103
src/contexts/AnnouncementProvider.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { AnnouncementVariant } from "@components/ui/AnnouncementBanner";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import md5 from "crypto-js/md5";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
|
||||
const initialAnnouncements: Announcement[] = [];
|
||||
|
||||
export interface Announcement extends AnnouncementVariant {
|
||||
tag: string;
|
||||
text: string;
|
||||
link?: string;
|
||||
linkText?: string;
|
||||
isExternal?: boolean;
|
||||
closeable: boolean;
|
||||
isCloudOnly: boolean;
|
||||
}
|
||||
|
||||
interface AnnouncementInfo extends Announcement {
|
||||
isOpen: boolean;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const AnnouncementContext = React.createContext(
|
||||
{} as {
|
||||
bannerHeight: number;
|
||||
announcements?: AnnouncementInfo[];
|
||||
closeAnnouncement: (hash: string) => void;
|
||||
setAnnouncements: React.Dispatch<
|
||||
React.SetStateAction<AnnouncementInfo[] | undefined>
|
||||
>;
|
||||
},
|
||||
);
|
||||
|
||||
const bannerHeight = 40;
|
||||
|
||||
export default function AnnouncementProvider({ children }: Props) {
|
||||
const [height, setHeight] = useState(0);
|
||||
const [closedAnnouncements, setClosedAnnouncements] = useLocalStorage<
|
||||
string[]
|
||||
>("netbird-closed-announcements", []);
|
||||
const [announcements, setAnnouncements] = useState<AnnouncementInfo[]>();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (announcements && announcements.length > 0) return;
|
||||
if (permission?.dashboard_view === "blocked") return;
|
||||
const initial = initialAnnouncements.map((announcement) => {
|
||||
const hash = md5(announcement.text).toString();
|
||||
const isOpen = !closedAnnouncements.some((h) => h === hash);
|
||||
return {
|
||||
...announcement,
|
||||
hash,
|
||||
isOpen,
|
||||
} as AnnouncementInfo;
|
||||
});
|
||||
if (initial.length > 0) {
|
||||
setAnnouncements(initial);
|
||||
}
|
||||
}, [closedAnnouncements, announcements]);
|
||||
|
||||
const closeAnnouncement = (hash: string) => {
|
||||
setClosedAnnouncements([...closedAnnouncements, hash]);
|
||||
setAnnouncements(() => {
|
||||
return announcements?.map((a) => {
|
||||
if (a.hash === hash) {
|
||||
return { ...a, isOpen: false };
|
||||
}
|
||||
return a;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isAnnouncementOpen = announcements?.some((a) => a.isOpen);
|
||||
if (isAnnouncementOpen) {
|
||||
setHeight(bannerHeight);
|
||||
} else {
|
||||
setHeight(0);
|
||||
}
|
||||
}, [announcements]);
|
||||
|
||||
return (
|
||||
<AnnouncementContext.Provider
|
||||
value={{
|
||||
bannerHeight: height,
|
||||
announcements,
|
||||
closeAnnouncement,
|
||||
setAnnouncements,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AnnouncementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAnnouncement = () => {
|
||||
return React.useContext(AnnouncementContext);
|
||||
};
|
||||
@@ -3,7 +3,14 @@ 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, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { User } from "@/interfaces/User";
|
||||
import type { NetbirdRelease } from "@/interfaces/Version";
|
||||
@@ -32,13 +39,27 @@ export default function ApplicationProvider({ children }: Props) {
|
||||
const userRequest = useApiCall<User[]>("/users", true);
|
||||
const [show, setShow] = useState(false);
|
||||
const requestCalled = useRef(false);
|
||||
const maxTries = 3;
|
||||
|
||||
const populateCache = useCallback(
|
||||
async (tries = 0) => {
|
||||
if (tries >= maxTries) {
|
||||
setShow(true);
|
||||
return Promise.reject();
|
||||
}
|
||||
try {
|
||||
await userRequest.get().then(() => setShow(true));
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
setTimeout(() => populateCache(tries + 1), 500);
|
||||
}
|
||||
},
|
||||
[userRequest, setShow],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestCalled.current) {
|
||||
userRequest
|
||||
.get()
|
||||
.then(() => setShow(true))
|
||||
.catch(() => setShow(true));
|
||||
populateCache().then();
|
||||
requestCalled.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import React, { useCallback } from "react";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Country } from "@/interfaces/Country";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
|
||||
@@ -16,9 +17,25 @@ const CountryContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function CountryProvider({ children }: Props) {
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
const getRegionByPeer = (peer: Peer) => "Unknown";
|
||||
|
||||
return permission?.dashboard_view != "full" ? (
|
||||
<CountryContext.Provider
|
||||
value={{ countries: [], isLoading: false, getRegionByPeer }}
|
||||
>
|
||||
{children}
|
||||
</CountryContext.Provider>
|
||||
) : (
|
||||
<CountryProviderContent>{children}</CountryProviderContent>
|
||||
);
|
||||
}
|
||||
|
||||
function CountryProviderContent({ children }: Props) {
|
||||
const { data: countries, isLoading } = useFetchApi<Country[]>(
|
||||
"/locations/countries",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
|
||||
@@ -81,16 +81,20 @@ export default function DialogProvider({ children }: Props) {
|
||||
/>
|
||||
|
||||
{dialogOptions.children && (
|
||||
<div className={"px-8 pt-4"}>{dialogOptions.children}</div>
|
||||
<div className={"px-8 pt-0"}>{dialogOptions.children}</div>
|
||||
)}
|
||||
|
||||
<ModalFooter className={"items-center gap-2"} separator={false}>
|
||||
<ModalFooter
|
||||
className={"items-center gap-2 pt-5"}
|
||||
separator={false}
|
||||
>
|
||||
<ModalClose asChild={true}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className={"w-full"}
|
||||
size={"sm"}
|
||||
tabIndex={-1}
|
||||
data-cy={"confirmation.cancel"}
|
||||
onClick={() => fn.current && fn.current(false)}
|
||||
>
|
||||
{dialogOptions.cancelText || "Cancel"}
|
||||
@@ -106,6 +110,7 @@ export default function DialogProvider({ children }: Props) {
|
||||
}
|
||||
className={"w-full"}
|
||||
size={"sm"}
|
||||
data-cy={"confirmation.confirm"}
|
||||
onClick={() => fn.current && fn.current(true)}
|
||||
>
|
||||
{dialogOptions.confirmText || "Confirm"}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
|
||||
type Props = {
|
||||
@@ -17,11 +19,22 @@ const GroupContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function GroupsProvider({ children }: Props) {
|
||||
const path = usePathname();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return path === "/peers" && permission.dashboard_view == "blocked" ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<GroupsProviderContent>{children}</GroupsProviderContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupsProviderContent({ children }: Props) {
|
||||
const { data: groups, mutate, isLoading } = useFetchApi<Group[]>("/groups");
|
||||
const [dropdownOptions, setDropdownOptions] = useState<Group[]>([]);
|
||||
|
||||
const refresh = () => {
|
||||
mutate().then();
|
||||
if (groups && !isLoading) mutate().then();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -77,9 +77,7 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
? loginExpiration
|
||||
: peer.login_expiration_enabled,
|
||||
approval_required:
|
||||
approval_required != undefined
|
||||
? approval_required
|
||||
: peer.approval_required,
|
||||
approval_required == undefined ? undefined : approval_required,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ const RoutesContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function RoutesProvider({ children }: Props) {
|
||||
const routeRequest = useApiCall<Route>("/routes");
|
||||
const routeRequest = useApiCall<Route>("/routes", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const updateRoute = async (
|
||||
@@ -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
|
||||
@@ -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 ?? [],
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useFetchApi from "@utils/api";
|
||||
import React, { useMemo } from "react";
|
||||
import { Permission } from "@/interfaces/Permission";
|
||||
import { User } from "@/interfaces/User";
|
||||
|
||||
type Props = {
|
||||
@@ -26,7 +27,7 @@ export default function UsersProvider({ children }: Props) {
|
||||
return users?.find((user) => user.is_current);
|
||||
}, [users]);
|
||||
|
||||
return !isLoading ? (
|
||||
return !isLoading && loggedInUser ? (
|
||||
<UsersContext.Provider value={{ users, refresh, loggedInUser }}>
|
||||
{children}
|
||||
</UsersContext.Provider>
|
||||
@@ -43,5 +44,19 @@ export const useLoggedInUser = () => {
|
||||
const isAdmin = loggedInUser ? loggedInUser?.role === "admin" : false;
|
||||
const isUser = !isOwner && !isAdmin;
|
||||
const isOwnerOrAdmin = isOwner || isAdmin;
|
||||
return { loggedInUser, isOwner, isAdmin, isUser, isOwnerOrAdmin } as const;
|
||||
|
||||
const permission = useMemo(() => {
|
||||
return {
|
||||
dashboard_view: loggedInUser?.permissions.dashboard_view || "blocked",
|
||||
} as Permission;
|
||||
}, [loggedInUser]);
|
||||
|
||||
return {
|
||||
loggedInUser,
|
||||
isOwner,
|
||||
isAdmin,
|
||||
isUser,
|
||||
isOwnerOrAdmin,
|
||||
permission,
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -13,10 +13,15 @@ export default function useOperatingSystem() {
|
||||
}
|
||||
|
||||
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.IOS as const;
|
||||
if (os.includes("win")) return OperatingSystem.WINDOWS as const;
|
||||
if (os.toLowerCase().includes("darwin"))
|
||||
return OperatingSystem.APPLE as const;
|
||||
if (os.toLowerCase().includes("mac")) return OperatingSystem.APPLE as const;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import loadConfig from "@utils/config";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
export const useRedirect = (
|
||||
url: string,
|
||||
replace: boolean = false,
|
||||
@@ -10,24 +11,43 @@ export const useRedirect = (
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const currentPath = usePathname();
|
||||
const callBackUrls = [config.redirectURI, config.silentRedirectURI];
|
||||
const callBackUrls = useRef([config.redirectURI, config.silentRedirectURI]);
|
||||
const isRedirecting = useRef(false);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable) return;
|
||||
if (callBackUrls.includes(url)) return; // Don't redirect to the callback urls to avoid infinite loop
|
||||
if (url === currentPath) return; // Don't redirect to the current page
|
||||
// If redirect is disabled or the url is already in the callback urls or the url is the current path then do not redirect
|
||||
if (!enable || callBackUrls.current.includes(url) || url === currentPath)
|
||||
return;
|
||||
|
||||
const redirect = replace ? router.replace : router.push; // Replace the current history or add a new one
|
||||
const performRedirect = () => {
|
||||
if (!isRedirecting.current) {
|
||||
isRedirecting.current = true;
|
||||
router.refresh();
|
||||
if (replace) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
isRedirecting.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
performRedirect();
|
||||
|
||||
// Timer in case the user has his browser tab open but not focused
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
}, 1000);
|
||||
// Try to redirect after 1.25 seconds if for whatever reason the redirect did not happen (network change, browser tab open but not focused etc.)
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (!isRedirecting.current) {
|
||||
performRedirect();
|
||||
}
|
||||
}, 1250);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [replace, router, url, enable]);
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [replace, router, url, enable, currentPath]);
|
||||
};
|
||||
|
||||
export default useRedirect;
|
||||
|
||||
@@ -10,5 +10,6 @@ export interface Account {
|
||||
jwt_groups_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
jwt_allow_groups: string[];
|
||||
regular_users_view_blocked: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
4
src/interfaces/Domain.ts
Normal file
4
src/interfaces/Domain.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Domain {
|
||||
id?: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -17,6 +17,14 @@ export interface AzureADIntegration {
|
||||
user_group_prefixes: string[];
|
||||
}
|
||||
|
||||
export interface OktaIntegration {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
group_prefixes: string[];
|
||||
user_group_prefixes: string[];
|
||||
auth_token: string;
|
||||
}
|
||||
|
||||
export interface IdentityProviderLog {
|
||||
id: number;
|
||||
level: string;
|
||||
|
||||
@@ -17,11 +17,6 @@ export interface Nameserver {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
id?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const NameserverPresets: Record<string, NameserverGroup> = {
|
||||
Default: {
|
||||
name: "",
|
||||
|
||||
3
src/interfaces/Permission.ts
Normal file
3
src/interfaces/Permission.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Permission {
|
||||
dashboard_view: "limited" | "full" | "blocked";
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface PostureCheck {
|
||||
os_version_check?: OperatingSystemVersionCheck;
|
||||
geo_location_check?: GeoLocationCheck;
|
||||
peer_network_range_check?: PeerNetworkRangeCheck;
|
||||
process_check?: ProcessCheck;
|
||||
};
|
||||
policies?: Policy[];
|
||||
active?: boolean;
|
||||
@@ -53,6 +54,17 @@ export interface PeerNetworkRangeCheck {
|
||||
action: "allow" | "deny";
|
||||
}
|
||||
|
||||
export interface ProcessCheck {
|
||||
processes: Process[];
|
||||
}
|
||||
|
||||
export interface Process {
|
||||
id: string;
|
||||
linux_path?: string;
|
||||
mac_path?: string;
|
||||
windows_path?: string;
|
||||
}
|
||||
|
||||
export const windowsKernelVersions: SelectOption[] = [
|
||||
{ value: "5.0", label: "Windows 2000" },
|
||||
{ value: "5.1", label: "Windows XP" },
|
||||
|
||||
@@ -3,26 +3,34 @@ export interface Route {
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
peer?: string;
|
||||
network: string;
|
||||
network?: string;
|
||||
domains?: string[];
|
||||
network_id: string;
|
||||
network_type?: string;
|
||||
metric?: number;
|
||||
masquerade: boolean;
|
||||
groups: string[];
|
||||
keep_route?: boolean;
|
||||
// Frontend only
|
||||
peer_groups?: string[];
|
||||
routesGroups?: string[];
|
||||
groupedRoutes?: GroupedRoute[];
|
||||
group_names?: string[];
|
||||
domain_search?: string;
|
||||
}
|
||||
|
||||
export interface GroupedRoute {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
network: string;
|
||||
network?: string;
|
||||
domains?: string[];
|
||||
keep_route?: boolean;
|
||||
network_id: string;
|
||||
high_availability_count: number;
|
||||
is_using_route_groups: boolean;
|
||||
routes?: Route[];
|
||||
group_names?: string[];
|
||||
description?: string;
|
||||
description_search?: string;
|
||||
domain_search?: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Permission } from "@/interfaces/Permission";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
@@ -9,6 +11,7 @@ export interface User {
|
||||
is_service_user?: boolean;
|
||||
is_blocked?: boolean;
|
||||
last_login?: Date;
|
||||
permissions: Permission;
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cn } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { Viewport } from "next/dist/lib/metadata/types/extra-types";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import OIDCProvider from "@/auth/OIDCProvider";
|
||||
@@ -16,7 +16,10 @@ import ErrorBoundaryProvider from "@/contexts/ErrorBoundary";
|
||||
import { GlobalThemeProvider } from "@/contexts/GlobalThemeProvider";
|
||||
import { NavigationEvents } from "@/contexts/NavigationEvents";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = localFont({
|
||||
src: "../assets/fonts/Inter.ttf",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// Extend dayjs with relativeTime plugin
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -9,14 +9,17 @@ import { useIsSm, useIsXs } from "@utils/responsive";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import AnnouncementProvider, {
|
||||
useAnnouncement,
|
||||
} from "@/contexts/AnnouncementProvider";
|
||||
import ApplicationProvider, {
|
||||
useApplicationContext,
|
||||
} from "@/contexts/ApplicationProvider";
|
||||
import CountryProvider from "@/contexts/CountryProvider";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import UsersProvider from "@/contexts/UsersProvider";
|
||||
import UsersProvider, { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import Navigation from "@/layouts/Navigation";
|
||||
import Navbar from "./Header";
|
||||
import Navbar, { headerHeight } from "./Header";
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
@@ -26,11 +29,13 @@ export default function DashboardLayout({
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<UsersProvider>
|
||||
<GroupsProvider>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
<AnnouncementProvider>
|
||||
<GroupsProvider>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
</AnnouncementProvider>
|
||||
</UsersProvider>
|
||||
</ApplicationProvider>
|
||||
);
|
||||
@@ -41,9 +46,10 @@ function DashboardPageContent({ children }: { children: React.ReactNode }) {
|
||||
const { mobileNavOpen, toggleMobileNav } = useApplicationContext();
|
||||
const isSm = useIsSm();
|
||||
const isXs = useIsXs();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
const navOpenPageWidth = isSm ? "50%" : isXs ? "65%" : "80%";
|
||||
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
return (
|
||||
<div className={cn("flex flex-col h-screen", mobileNavOpen && "flex")}>
|
||||
{mobileNavOpen && (
|
||||
@@ -145,13 +151,16 @@ function DashboardPageContent({ children }: { children: React.ReactNode }) {
|
||||
}}
|
||||
>
|
||||
<Navbar />
|
||||
|
||||
<div
|
||||
className={"flex flex-row flex-grow"}
|
||||
style={{
|
||||
height: "calc(100vh - 75px)",
|
||||
height: `calc(100vh - ${headerHeight + bannerHeight}px)`,
|
||||
}}
|
||||
>
|
||||
<Navigation hideOnMobile />
|
||||
{permission.dashboard_view !== "blocked" && (
|
||||
<Navigation hideOnMobile />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,59 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import { AnnouncementBanner } from "@components/ui/AnnouncementBanner";
|
||||
import DarkModeToggle from "@components/ui/DarkModeToggle";
|
||||
import UserDropdown from "@components/ui/UserDropdown";
|
||||
import { Navbar } from "flowbite-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import NetBirdLogo from "@/assets/netbird.svg";
|
||||
import NetBirdLogoFull from "@/assets/netbird-full.svg";
|
||||
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
|
||||
import { useApplicationContext } from "@/contexts/ApplicationProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
|
||||
export const headerHeight = 75;
|
||||
|
||||
export default function NavbarWithDropdown() {
|
||||
const router = useRouter();
|
||||
const Logo = useMemo(() => {
|
||||
return <Image src={NetBirdLogo} width={30} alt={"NetBird Logo"} />;
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={NetBirdLogoFull}
|
||||
height={22}
|
||||
alt={"NetBird Logo"}
|
||||
className={"hidden md:block"}
|
||||
/>
|
||||
<Image
|
||||
src={NetBirdLogo}
|
||||
width={30}
|
||||
alt={"NetBird Logo"}
|
||||
className={"md:hidden"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const { toggleMobileNav } = useApplicationContext();
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar
|
||||
fluid
|
||||
className={
|
||||
"border-b dark:border-zinc-700/40 fixed z-50 h-[75px] px-3 md:px-4 w-full"
|
||||
}
|
||||
<div
|
||||
className={"fixed z-50 w-full"}
|
||||
style={{
|
||||
height: headerHeight + bannerHeight,
|
||||
}}
|
||||
>
|
||||
<div className={"flex items-center gap-4 md:hidden"}>
|
||||
<Button
|
||||
className={"!px-3 md:hidden"}
|
||||
variant={"default-outline"}
|
||||
onClick={toggleMobileNav}
|
||||
>
|
||||
<div>
|
||||
<MenuIcon size={20} className={"relative"} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Navbar.Brand
|
||||
onClick={() => router.push("/peers")}
|
||||
className={"cursor-pointer hover:opacity-70 transition-all"}
|
||||
<AnnouncementBanner />
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white px-2 py-4 dark:border-gray-700 dark:bg-nb-gray/50 backdrop-blur-lg sm:px-6",
|
||||
"border-b dark:border-zinc-700/40 px-3 md:px-4 w-full",
|
||||
"flex justify-between items-center transition-all",
|
||||
)}
|
||||
>
|
||||
{Logo}
|
||||
</Navbar.Brand>
|
||||
|
||||
<div className="flex md:order-2 gap-4">
|
||||
<div className={"hidden md:block"}>
|
||||
<DarkModeToggle />
|
||||
<div className={"flex items-center gap-4 md:hidden"}>
|
||||
<Button
|
||||
className={cn(
|
||||
"!px-3 md:hidden",
|
||||
permission.dashboard_view == "blocked" &&
|
||||
"opacity-0 pointer-events-none",
|
||||
)}
|
||||
variant={"default-outline"}
|
||||
onClick={toggleMobileNav}
|
||||
>
|
||||
<div>
|
||||
<MenuIcon size={20} className={"relative"} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => router.push("/peers")}
|
||||
className={"cursor-pointer hover:opacity-70 transition-all"}
|
||||
>
|
||||
{Logo}
|
||||
</div>
|
||||
|
||||
<UserDropdown />
|
||||
<div className="flex md:order-2 gap-4">
|
||||
<div className={"hidden md:block"}>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</Navbar>
|
||||
<div className={"h-[75px]"}></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: headerHeight + bannerHeight,
|
||||
}}
|
||||
></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
import { CustomFlowbiteTheme, Sidebar } from "flowbite-react";
|
||||
import { SidebarItemGroupProps } from "flowbite-react/lib/esm/components/Sidebar/SidebarItemGroup";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import DocsIcon from "@/assets/icons/DocsIcon";
|
||||
import IntegrationIcon from "@/assets/icons/IntegrationIcon";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||
import SidebarItem from "@/components/SidebarItem";
|
||||
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { headerHeight } from "@/layouts/Header";
|
||||
|
||||
const customTheme: CustomFlowbiteTheme["sidebar"] = {
|
||||
root: {
|
||||
@@ -34,6 +34,7 @@ export default function Navigation({
|
||||
hideOnMobile = false,
|
||||
}: Props) {
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
@@ -42,123 +43,133 @@ export default function Navigation({
|
||||
hideOnMobile ? "hidden md:block" : "",
|
||||
fullWidth
|
||||
? "w-auto max-w-[22rem]"
|
||||
: "w-[15rem] min-w-[15rem] overflow-y-auto",
|
||||
: "w-[15rem] max-w-[15rem] min-w-[15rem] overflow-y-auto",
|
||||
)}
|
||||
theme={customTheme}
|
||||
style={{
|
||||
height: fullWidth ? "calc(100vh - 75px)" : "100%",
|
||||
height: fullWidth
|
||||
? `calc(100vh - ${headerHeight + bannerHeight}px)`
|
||||
: "100%",
|
||||
}}
|
||||
>
|
||||
<Sidebar.Items className={cn(fullWidth ? "w-10/12" : "fixed")}>
|
||||
<Sidebar.Items className={cn(fullWidth ? "w-10/12" : "fixed h-full")}>
|
||||
<ScrollArea
|
||||
style={{
|
||||
height: !fullWidth ? "calc(100vh - 75px)" : "100%",
|
||||
height: !fullWidth
|
||||
? `calc(100vh - ${headerHeight + bannerHeight}px)`
|
||||
: "100%",
|
||||
}}
|
||||
className={"pt-4"}
|
||||
>
|
||||
<SidebarItemGroup>
|
||||
<SidebarItem icon={<PeerIcon />} label="Peers" href={"/peers"} />
|
||||
|
||||
{!isUser && (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col justify-between pt-4 w-[15rem] max-w-[15rem] min-w-[15rem]"
|
||||
}
|
||||
style={{
|
||||
height: !fullWidth
|
||||
? `calc(100vh - ${headerHeight + bannerHeight}px)`
|
||||
: "100%",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<SidebarItemGroup>
|
||||
<SidebarItem
|
||||
icon={<SetupKeysIcon />}
|
||||
label="Setup Keys"
|
||||
href={"/setup-keys"}
|
||||
icon={<PeerIcon />}
|
||||
label="Peers"
|
||||
href={"/peers"}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<AccessControlIcon />}
|
||||
label="Access Control"
|
||||
collapsible
|
||||
>
|
||||
|
||||
{!isUser && (
|
||||
<>
|
||||
<SidebarItem
|
||||
icon={<SetupKeysIcon />}
|
||||
label="Setup Keys"
|
||||
href={"/setup-keys"}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<AccessControlIcon />}
|
||||
label="Access Control"
|
||||
collapsible
|
||||
>
|
||||
<SidebarItem
|
||||
label="Policies"
|
||||
href={"/access-control"}
|
||||
isChild
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
<SidebarItem
|
||||
label="Posture Checks"
|
||||
isChild
|
||||
href={"/posture-checks"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
</SidebarItem>
|
||||
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
label="Network Routes"
|
||||
href={"/network-routes"}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<DNSIcon />}
|
||||
label="DNS"
|
||||
collapsible
|
||||
exactPathMatch={true}
|
||||
>
|
||||
<SidebarItem
|
||||
label="Nameservers"
|
||||
isChild
|
||||
href={"/dns/nameservers"}
|
||||
/>
|
||||
<SidebarItem
|
||||
label="DNS Settings"
|
||||
isChild
|
||||
href={"/dns/settings"}
|
||||
/>
|
||||
</SidebarItem>
|
||||
<SidebarItem icon={<TeamIcon />} label="Team" collapsible>
|
||||
<SidebarItem label="Users" isChild href={"/team/users"} />
|
||||
<SidebarItem
|
||||
label="Service Users"
|
||||
isChild
|
||||
href={"/team/service-users"}
|
||||
/>
|
||||
</SidebarItem>
|
||||
<SidebarItem
|
||||
icon={<ActivityIcon />}
|
||||
label="Activity"
|
||||
href={"/activity"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isUser && (
|
||||
<SidebarItem
|
||||
label="Policies"
|
||||
href={"/access-control"}
|
||||
isChild
|
||||
icon={<DocsIcon />}
|
||||
href={"https://docs.netbird.io/"}
|
||||
target={"_blank"}
|
||||
label="Documentation"
|
||||
/>
|
||||
)}
|
||||
</SidebarItemGroup>
|
||||
{!isUser && (
|
||||
<SidebarItemGroup>
|
||||
<SidebarItem
|
||||
icon={<SettingsIcon />}
|
||||
label="Settings"
|
||||
href={"/settings"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
<SidebarItem
|
||||
label="Posture Checks"
|
||||
isChild
|
||||
href={"/posture-checks"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
</SidebarItem>
|
||||
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
label="Network Routes"
|
||||
href={"/network-routes"}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<DNSIcon />}
|
||||
label="DNS"
|
||||
collapsible
|
||||
exactPathMatch={true}
|
||||
>
|
||||
<SidebarItem
|
||||
label="Nameservers"
|
||||
isChild
|
||||
href={"/dns/nameservers"}
|
||||
icon={<DocsIcon />}
|
||||
href={"https://docs.netbird.io/"}
|
||||
target={"_blank"}
|
||||
label="Documentation"
|
||||
/>
|
||||
<SidebarItem
|
||||
label="DNS Settings"
|
||||
isChild
|
||||
href={"/dns/settings"}
|
||||
/>
|
||||
</SidebarItem>
|
||||
<SidebarItem icon={<TeamIcon />} label="Team" collapsible>
|
||||
<SidebarItem label="Users" isChild href={"/team/users"} />
|
||||
<SidebarItem
|
||||
label="Service Users"
|
||||
isChild
|
||||
href={"/team/service-users"}
|
||||
/>
|
||||
</SidebarItem>
|
||||
<SidebarItem
|
||||
icon={<ActivityIcon />}
|
||||
label="Activity"
|
||||
href={"/activity"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isUser && (
|
||||
<SidebarItem
|
||||
icon={<DocsIcon />}
|
||||
href={"https://docs.netbird.io/"}
|
||||
target={"_blank"}
|
||||
label="Documentation"
|
||||
/>
|
||||
)}
|
||||
</SidebarItemGroup>
|
||||
|
||||
{!isUser && (
|
||||
<SidebarItemGroup>
|
||||
<SidebarItem
|
||||
icon={<SettingsIcon />}
|
||||
label="Settings"
|
||||
href={"/settings"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
|
||||
{(isLocalDev() || isNetBirdHosted()) && (
|
||||
<SidebarItem
|
||||
icon={<IntegrationIcon />}
|
||||
label="Integrations"
|
||||
href={"/integrations"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
</SidebarItemGroup>
|
||||
)}
|
||||
|
||||
<SidebarItem
|
||||
icon={<DocsIcon />}
|
||||
href={"https://docs.netbird.io/"}
|
||||
target={"_blank"}
|
||||
label="Documentation"
|
||||
/>
|
||||
</SidebarItemGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Sidebar.Items>
|
||||
</Sidebar>
|
||||
@@ -167,7 +178,10 @@ export default function Navigation({
|
||||
|
||||
export function SidebarItemGroup(props: SidebarItemGroupProps) {
|
||||
return (
|
||||
<Sidebar.ItemGroup className={"dark:border-zinc-700/40"} {...props}>
|
||||
<Sidebar.ItemGroup
|
||||
className={"dark:border-zinc-700/40 space-y-1.5"}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Sidebar.ItemGroup>
|
||||
);
|
||||
|
||||
@@ -239,12 +239,6 @@ export function AccessControlModalContent({
|
||||
|
||||
const portAndDirectionDisabled = protocol == "icmp" || protocol == "all";
|
||||
|
||||
const buttonDisabled = useMemo(() => {
|
||||
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
|
||||
if (name.length == 0) return true;
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports, name]);
|
||||
|
||||
const [postureChecks, setPostureChecks] = useState<PostureCheck[]>([]);
|
||||
const postureChecksLoaded = useRef(false);
|
||||
|
||||
@@ -268,6 +262,16 @@ export function AccessControlModalContent({
|
||||
}
|
||||
}, [initialPostureChecks]);
|
||||
|
||||
const continuePostureChecksDisabled = useMemo(() => {
|
||||
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports]);
|
||||
|
||||
const submitDisabled = useMemo(() => {
|
||||
if (name.length == 0) return true;
|
||||
if (continuePostureChecksDisabled) return true;
|
||||
}, [name, continuePostureChecksDisabled]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-2xl"}>
|
||||
<ModalHeader
|
||||
@@ -283,14 +287,17 @@ export function AccessControlModalContent({
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"policy"}>
|
||||
<ArrowRightLeft size={16} />
|
||||
Policy
|
||||
</TabsTrigger>
|
||||
<PostureCheckTabTrigger />
|
||||
<TabsTrigger value={"general"}>
|
||||
<PostureCheckTabTrigger disabled={continuePostureChecksDisabled} />
|
||||
<TabsTrigger
|
||||
value={"general"}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
@@ -456,24 +463,74 @@ export function AccessControlModalContent({
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{!policy ? (
|
||||
<>
|
||||
{tab == "policy" && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={buttonDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
{policy ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Policy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{tab == "posture_checks" && (
|
||||
<Button variant={"secondary"} onClick={() => setTab("policy")}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "policy" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("posture_checks")}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "posture_checks" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "general" && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("posture_checks")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={submitDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Policy
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={submitDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -3,7 +3,11 @@ import { Label } from "@components/Label";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isLocalDev, isProduction } from "@utils/netbird";
|
||||
import { isEmpty } from "lodash";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import React, { useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||
|
||||
type Props = {
|
||||
@@ -31,6 +35,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
/**
|
||||
* Setup Key
|
||||
*/
|
||||
|
||||
if (event.activity_code == "setupkey.revoke")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -50,14 +58,8 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "setupkey.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "dashboard.login")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m.username}</Value> logged in to the dashboard
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,6 +79,20 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Dashboard
|
||||
*/
|
||||
if (event.activity_code == "dashboard.login")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m.username}</Value> logged in to the dashboard
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Policy
|
||||
*/
|
||||
|
||||
if (event.activity_code == "policy.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -98,48 +114,68 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "route.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was deleted
|
||||
</div>
|
||||
);
|
||||
/**
|
||||
* Route
|
||||
*/
|
||||
|
||||
if (event.activity_code == "route.update")
|
||||
if (event.activity_code == "route.delete") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was updated
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was deleted
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.activity_code == "route.add")
|
||||
if (event.activity_code == "route.update") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was created
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was updated
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.activity_code == "route.add") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was created
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User
|
||||
*/
|
||||
|
||||
if (event.activity_code == "user.peer.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was deleted
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was updated
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -150,86 +186,6 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was removed from the peer with the ip{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was added to the peer with the ip{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expire")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login of the peer <Value>{m.name}</Value> is expired
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.ssh.disable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
SSH Server of peer <Value>{m.name}</Value> was disabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.ssh.enable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
SSH Server of peer <Value>{m.name}</Value> was enabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expiration.disable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login expiration of peer <Value>{m.name}</Value> was disabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expiration.enable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login expiration of peer <Value>{m.name}</Value> was enabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.rename")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the ip <Value>{m.ip}</Value> was renamed to{" "}
|
||||
<Value>{m.name}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.approve")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the ip <Value>{m.ip}</Value> was approved
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.name}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "account.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{event.initiator_name}</Value> created an account
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.invite")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -246,52 +202,6 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "nameserver.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was added
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "nameserver.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "nameserver.group.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.update")
|
||||
return <div className={"inline"}>Global login expiration was updated</div>;
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.enable")
|
||||
return <div className={"inline"}>Global login expiration was enabled</div>;
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.disable")
|
||||
return <div className={"inline"}>Global login expiration was disabled</div>;
|
||||
|
||||
if (event.activity_code == "personal.access.token.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Access token <Value>{event.meta.name}</Value> for user{" "}
|
||||
<Value>{event.meta.username}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "personal.access.token.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Access token <Value>{event.meta.name}</Value> for user{" "}
|
||||
<Value>{event.meta.username}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.block")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -334,6 +244,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Service User
|
||||
*/
|
||||
|
||||
if (event.activity_code == "service.user.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -348,6 +262,87 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Peer
|
||||
*/
|
||||
|
||||
if (event.activity_code == "peer.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was removed from the peer with the
|
||||
NetBird IP <Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was added to the peer with the NetBird IP{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expire")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login of the peer <Value>{m.name}</Value> is expired
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.ssh.disable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
SSH Server of peer <Value>{m.name}</Value> was disabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.ssh.enable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
SSH Server of peer <Value>{m.name}</Value> was enabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expiration.disable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login expiration of peer <Value>{m.name}</Value> was disabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.login.expiration.enable")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Login expiration of peer <Value>{m.name}</Value> was enabled
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.rename")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was renamed to{" "}
|
||||
<Value>{m.name}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.approve")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was approved
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Group
|
||||
*/
|
||||
|
||||
if (event.activity_code == "group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.name}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -355,6 +350,75 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Account
|
||||
*/
|
||||
|
||||
if (event.activity_code == "account.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{event.initiator_name}</Value> created an account
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.update")
|
||||
return <div className={"inline"}>Global login expiration was updated</div>;
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.enable")
|
||||
return <div className={"inline"}>Global login expiration was enabled</div>;
|
||||
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.disable")
|
||||
return <div className={"inline"}>Global login expiration was disabled</div>;
|
||||
|
||||
/**
|
||||
* Nameserver
|
||||
*/
|
||||
|
||||
if (event.activity_code == "nameserver.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was added
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "nameserver.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "nameserver.group.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Nameserver <Value>{event.meta.name}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Personal Access Token
|
||||
*/
|
||||
|
||||
if (event.activity_code == "personal.access.token.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Access token <Value>{event.meta.name}</Value> for user{" "}
|
||||
<Value>{event.meta.username}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "personal.access.token.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Access token <Value>{event.meta.name}</Value> for user{" "}
|
||||
<Value>{event.meta.username}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
|
||||
if (event.activity_code == "integration.create") {
|
||||
if (!event.meta.platform) return "Integration created";
|
||||
return (
|
||||
@@ -385,7 +449,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// Group was added to DNS Management Setting that disables DNS for the group
|
||||
/**
|
||||
* DNS
|
||||
*/
|
||||
|
||||
if (event.activity_code == "dns.setting.disabled.management.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -402,15 +469,71 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO add activity texts
|
||||
// rule.add
|
||||
// rule.update
|
||||
// rule.delete
|
||||
// setupkey.update
|
||||
// setupkey.overuse
|
||||
// group.update
|
||||
// group.delete
|
||||
// user.peer.login
|
||||
/**
|
||||
* Posture Checks
|
||||
*/
|
||||
|
||||
if (event.activity_code == "posture.check.updated")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Posture check <Value> {m.name}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "posture.check.created")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Posture check <Value> {m.name}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "posture.check.deleted")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Posture check <Value> {m.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "transferred.owner.role")
|
||||
return <div className={"inline"}>Owner role was transferred</div>;
|
||||
|
||||
/**
|
||||
* EDR
|
||||
*/
|
||||
if (event.activity_code == "integrated-validator.api.created")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.api.updated")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration updated
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.api.deleted")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.host-check.approved")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer approved by <Value>{m?.platform}</Value> integration
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.host-check.denied")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer rejected by <Value>{m?.platform}</Value> integration
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2.5 items-center"}>
|
||||
@@ -453,7 +576,7 @@ function Value({
|
||||
return children ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-nb-gray-200 inline font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||
"text-nb-gray-200 inline-flex gap-1 items-center max-h-[22px] font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -461,3 +584,40 @@ function Value({
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function PeerConnectionInfo({ meta }: { meta: any }) {
|
||||
const hasMeta =
|
||||
!isEmpty(meta?.location_country_code) ||
|
||||
!isEmpty(meta?.location_connection_ip);
|
||||
const { countries } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
if (!countries) return "Unknown";
|
||||
const country = countries.find(
|
||||
(c) => c.country_code === meta?.location_country_code,
|
||||
);
|
||||
if (!country) return "Unknown";
|
||||
if (!meta?.location_city_name) return country.country_name;
|
||||
return `${country.country_name}, ${meta?.location_city_name}`;
|
||||
}, [countries, meta]);
|
||||
|
||||
return hasMeta ? (
|
||||
<>
|
||||
{" "}
|
||||
from{" "}
|
||||
{meta?.location_connection_ip && (
|
||||
<Value>{meta?.location_connection_ip}</Value>
|
||||
)}{" "}
|
||||
{meta?.location_country_code && (
|
||||
<Value>
|
||||
{isEmpty(meta?.location_country_code) ? (
|
||||
<GlobeIcon size={9} className={"text-nb-gray-300"} />
|
||||
) : (
|
||||
<RoundedFlag country={meta?.location_country_code} size={9} />
|
||||
)}
|
||||
{countryText}
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ArrowLeftRight,
|
||||
Blocks,
|
||||
Cog,
|
||||
CreditCardIcon,
|
||||
FolderGit2,
|
||||
Globe,
|
||||
HelpCircleIcon,
|
||||
@@ -10,8 +11,10 @@ import {
|
||||
LogIn,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
RefreshCcw,
|
||||
Server,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -70,6 +73,22 @@ export default function ActivityTypeIcon({
|
||||
return <User size={size} className={cn(DEFAULT_CLASSES, className)} />;
|
||||
} else if (code.startsWith("service")) {
|
||||
return <Cog size={size} className={cn(DEFAULT_CLASSES, className)} />;
|
||||
} else if (code.startsWith("billing")) {
|
||||
return (
|
||||
<CreditCardIcon size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("integrated")) {
|
||||
return (
|
||||
<ShieldCheck size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("posture")) {
|
||||
return (
|
||||
<ShieldCheck size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("transferred")) {
|
||||
return (
|
||||
<RefreshCcw size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HelpCircleIcon size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
|
||||
@@ -10,6 +10,7 @@ type Props = {
|
||||
leftSection?: React.ReactNode;
|
||||
text?: string | React.ReactNode;
|
||||
className?: string;
|
||||
additionalInfo?: React.ReactNode;
|
||||
};
|
||||
export default function ActiveInactiveRow({
|
||||
active,
|
||||
@@ -18,11 +19,12 @@ export default function ActiveInactiveRow({
|
||||
leftSection,
|
||||
inactiveDot = "gray",
|
||||
className,
|
||||
additionalInfo,
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-3 dark:text-neutral-300 text-neutral-500 min-w-[250px] max-w-[250px]",
|
||||
"gap-3 dark:text-neutral-300 text-neutral-500 min-w-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -34,9 +36,12 @@ export default function ActiveInactiveRow({
|
||||
inactiveDot={inactiveDot}
|
||||
className={"mt-1 shrink-0"}
|
||||
/>
|
||||
<div className={"flex flex-col"}>
|
||||
<div className={" font-medium"}>
|
||||
<div className={"flex flex-col min-w-0"}>
|
||||
<div
|
||||
className={"font-medium flex gap-2 items-center justify-center"}
|
||||
>
|
||||
<TextWithTooltip text={text as string} maxChars={25} />
|
||||
{additionalInfo}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
Modal,
|
||||
@@ -10,10 +11,12 @@ import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import Separator from "@components/Separator";
|
||||
import MultipleGroups from "@components/ui/MultipleGroups";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { FolderGit2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
@@ -26,6 +29,7 @@ type Props = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
peer?: Peer;
|
||||
showAddGroupButton?: boolean;
|
||||
};
|
||||
|
||||
export default function GroupsRow({
|
||||
@@ -36,8 +40,10 @@ export default function GroupsRow({
|
||||
label = "Assigned Groups",
|
||||
description = "Use groups to control what this peer can access",
|
||||
peer,
|
||||
showAddGroupButton = false,
|
||||
}: Props) {
|
||||
const { groups: allGroups } = useGroups();
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
// Get the group by the id
|
||||
const foundGroups = useMemo(() => {
|
||||
@@ -54,10 +60,17 @@ export default function GroupsRow({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setModal && setModal(true);
|
||||
setModal && !isUser && setModal(true);
|
||||
}}
|
||||
>
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
{foundGroups?.length == 0 && showAddGroupButton ? (
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<IconCirclePlus size={14} />
|
||||
Add Groups
|
||||
</Badge>
|
||||
) : (
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
)}
|
||||
</ModalTrigger>
|
||||
<EditGroupsModal
|
||||
groups={foundGroups}
|
||||
|
||||
@@ -17,8 +17,9 @@ import Paragraph from "@components/Paragraph";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import InputDomain, { domainReducer } from "@components/ui/InputDomain";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { cn } from "@utils/helpers";
|
||||
import cidr from "ip-cidr";
|
||||
import { uniqueId } from "lodash";
|
||||
import {
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
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 { Nameserver, NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
|
||||
type Props = {
|
||||
@@ -97,25 +98,12 @@ enum ActionType {
|
||||
UPDATE = "UPDATE",
|
||||
}
|
||||
|
||||
export const domainReducer = (state: Domain[], action: any) => {
|
||||
switch (action.type) {
|
||||
case ActionType.ADD:
|
||||
return [...state, { name: "", id: uniqueId("ns") }];
|
||||
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 function NameserverModalContent({
|
||||
onSuccess,
|
||||
preset,
|
||||
cell,
|
||||
}: ModalProps) {
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers");
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const isUpdate = useMemo(() => {
|
||||
@@ -199,7 +187,7 @@ export function NameserverModalContent({
|
||||
// Domains
|
||||
const [domains, setDomains] = useReducer(domainReducer, [], () => {
|
||||
if (preset?.domains?.length) {
|
||||
return preset.domains.map((d) => ({ name: d, id: uniqueId("ns") }));
|
||||
return preset.domains.map((d) => ({ name: d, id: uniqueId("domain") }));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
@@ -233,25 +221,27 @@ export function NameserverModalContent({
|
||||
return domains.some((d) => d.name === "");
|
||||
}, [domains]);
|
||||
|
||||
const hasAnyError = useMemo(() => {
|
||||
return (
|
||||
const nameLengthError = useMemo(() => {
|
||||
if (name.length > 40) return "Name should be less than 40 characters";
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
const canContinueToDomains = useMemo(() => {
|
||||
return !(
|
||||
hasNSErrors ||
|
||||
nsError ||
|
||||
domainError ||
|
||||
name == "" ||
|
||||
nameservers.length == 0 ||
|
||||
hasDomainErrors ||
|
||||
groups.length == 0
|
||||
);
|
||||
}, [
|
||||
nsError,
|
||||
domainError,
|
||||
name,
|
||||
nameservers,
|
||||
groups,
|
||||
hasNSErrors,
|
||||
hasDomainErrors,
|
||||
]);
|
||||
}, [hasNSErrors, nsError, nameservers.length, groups.length]);
|
||||
|
||||
const canContinueToGeneral = useMemo(() => {
|
||||
return !(!canContinueToDomains || domainError || hasDomainErrors);
|
||||
}, [canContinueToDomains, domainError, hasDomainErrors]);
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
return !(!canContinueToGeneral || nameLengthError !== "" || name == "");
|
||||
}, [canContinueToGeneral, nameLengthError, name]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
@@ -262,7 +252,7 @@ export function NameserverModalContent({
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"nameserver"}>
|
||||
<ServerIcon
|
||||
@@ -273,7 +263,7 @@ export function NameserverModalContent({
|
||||
/>
|
||||
Nameserver
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"domains"}>
|
||||
<TabsTrigger value={"domains"} disabled={!canContinueToDomains}>
|
||||
<GlobeIcon
|
||||
size={16}
|
||||
className={
|
||||
@@ -282,7 +272,7 @@ export function NameserverModalContent({
|
||||
/>
|
||||
Domains
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"general"}>
|
||||
<TabsTrigger value={"general"} disabled={!canContinueToGeneral}>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
@@ -368,7 +358,7 @@ export function NameserverModalContent({
|
||||
<div className={"flex flex-col gap-2 w-full"}>
|
||||
{domains.map((domain, i) => {
|
||||
return (
|
||||
<DomainInput
|
||||
<InputDomain
|
||||
key={domain.id}
|
||||
value={domain}
|
||||
onChange={(d) =>
|
||||
@@ -427,6 +417,7 @@ export function NameserverModalContent({
|
||||
<Input
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
error={nameLengthError}
|
||||
placeholder={"e.g., Public DNS"}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -465,20 +456,77 @@ export function NameserverModalContent({
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{!isUpdate ? (
|
||||
<>
|
||||
{tab == "nameserver" && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
<Button variant={"primary"} disabled={hasAnyError} onClick={submit}>
|
||||
{isUpdate ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Nameserver
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{tab == "domains" && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("nameserver")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "nameserver" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("domains")}
|
||||
disabled={!canContinueToDomains}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "domains" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={!canContinueToGeneral}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "general" && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("domains")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canSubmit}
|
||||
onClick={submit}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Nameserver
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canSubmit}
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
@@ -516,7 +564,7 @@ function NameserverInput({
|
||||
const validCIDR = cidr.isValidAddress(ip);
|
||||
if (!validCIDR) {
|
||||
onError && onError(true);
|
||||
return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
|
||||
return "Please enter a valid IP, e.g., 192.168.1.0";
|
||||
}
|
||||
onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -532,7 +580,7 @@ function NameserverInput({
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={"IP"}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
placeholder={"e.g., 172.16.0.0"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={ip}
|
||||
className={"font-mono !text-[13px]"}
|
||||
@@ -559,63 +607,3 @@ function NameserverInput({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainInput({
|
||||
value,
|
||||
onChange,
|
||||
onRemove,
|
||||
onError,
|
||||
}: {
|
||||
value: Domain;
|
||||
onChange: (d: Domain) => void;
|
||||
onRemove: () => void;
|
||||
onError?: (error: boolean) => void;
|
||||
error?: string;
|
||||
}) {
|
||||
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) {
|
||||
onError && onError(true);
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
onError && onError(false);
|
||||
}, [name, onError]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
46
src/modules/exit-node/AddExitNodeButton.tsx
Normal file
46
src/modules/exit-node/AddExitNodeButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Button from "@components/Button";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
|
||||
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||
|
||||
type Props = {
|
||||
peer?: Peer;
|
||||
firstTime?: boolean;
|
||||
};
|
||||
export const AddExitNodeButton = ({ peer, firstTime = false }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExitNodeHelpTooltip>
|
||||
<Button variant={"secondary"} onClick={() => setModal(true)}>
|
||||
{!firstTime ? (
|
||||
<>
|
||||
<IconCirclePlus size={16} />
|
||||
Add Exit Node
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconDirectionSign size={16} className={"text-yellow-400"} />
|
||||
Set Up Exit Node
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ExitNodeHelpTooltip>
|
||||
<Modal open={modal} onOpenChange={setModal}>
|
||||
{modal && (
|
||||
<RouteModalContent
|
||||
onSuccess={() => setModal(false)}
|
||||
peer={peer}
|
||||
isFirstExitNode={firstTime}
|
||||
exitNode={true}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
59
src/modules/exit-node/ExitNodeDropdownButton.tsx
Normal file
59
src/modules/exit-node/ExitNodeDropdownButton.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
|
||||
return isLinux ? (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => setModal(true)}>
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
{hasExitNodes ? (
|
||||
<>
|
||||
<IconCirclePlus size={14} className={"shrink-0"} />
|
||||
<div className={"flex justify-between items-center w-full"}>
|
||||
Add Exit Node
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconDirectionSign
|
||||
size={14}
|
||||
className={"shrink-0 text-yellow-400"}
|
||||
/>
|
||||
<div className={"flex justify-between items-center w-full"}>
|
||||
Set Up Exit Node
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<Modal open={modal} onOpenChange={setModal}>
|
||||
{modal && (
|
||||
<RoutesProvider>
|
||||
<RouteModalContent
|
||||
onSuccess={() => setModal(false)}
|
||||
peer={peer}
|
||||
exitNode={true}
|
||||
/>
|
||||
</RoutesProvider>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
47
src/modules/exit-node/ExitNodeHelpTooltip.tsx
Normal file
47
src/modules/exit-node/ExitNodeHelpTooltip.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
hoverButton?: boolean;
|
||||
};
|
||||
export const ExitNodeHelpTooltip = ({
|
||||
children,
|
||||
hoverButton = false,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FullTooltip
|
||||
hoverButton={hoverButton}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
An exit node is a network route that routes all your internet
|
||||
traffic through one of your peers.
|
||||
<div className={"mt-2"}>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/configuring-default-routes-for-internet-traffic"
|
||||
}
|
||||
target={"_blank"}
|
||||
className={"mr-1"}
|
||||
>
|
||||
Exit Nodes
|
||||
<ExternalLinkIcon size={10} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
src/modules/exit-node/ExitNodePeerIndicator.tsx
Normal file
25
src/modules/exit-node/ExitNodePeerIndicator.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
export const ExitNodePeerIndicator = ({ peer }: Props) => {
|
||||
const hasExitNode = useHasExitNodes(peer);
|
||||
|
||||
return hasExitNode ? (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
This peer is an exit node. Traffic from the configured distribution
|
||||
groups will be routed through this peer.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconDirectionSign size={15} className={"text-yellow-400 shrink-0"} />
|
||||
</FullTooltip>
|
||||
) : null;
|
||||
};
|
||||
19
src/modules/exit-node/useHasExitNodes.tsx
Normal file
19
src/modules/exit-node/useHasExitNodes.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
|
||||
export const useHasExitNodes = (peer?: Peer) => {
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
const { data: routes } = useFetchApi<Route[]>(
|
||||
`/routes`,
|
||||
false,
|
||||
true,
|
||||
isOwnerOrAdmin,
|
||||
);
|
||||
return peer
|
||||
? routes?.some(
|
||||
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
|
||||
) || false
|
||||
: false;
|
||||
};
|
||||
@@ -142,7 +142,8 @@ export function GroupSelector({
|
||||
<div className={""}>
|
||||
<div className={"grid grid-cols-1 gap-1"}>
|
||||
{orderBy(groups, "name")?.map((item) => {
|
||||
const value = item.name;
|
||||
const value = item?.name || "";
|
||||
if (value === "") return null;
|
||||
const isSelected =
|
||||
values.find((c) => c == value) != undefined;
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ExternalLinkIcon, Repeat } from "lucide-react";
|
||||
import { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
|
||||
type Props<T> = {
|
||||
image: StaticImport | string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: {
|
||||
title: string;
|
||||
href: string;
|
||||
};
|
||||
data?: T;
|
||||
switchState: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
onSetup?: () => void;
|
||||
disabled?: boolean;
|
||||
hideSwitch?: boolean;
|
||||
};
|
||||
|
||||
export function IntegrationCard<T>({
|
||||
image,
|
||||
name,
|
||||
description,
|
||||
url,
|
||||
data,
|
||||
switchState,
|
||||
onEnabledChange,
|
||||
children,
|
||||
onSetup,
|
||||
disabled,
|
||||
hideSwitch = false,
|
||||
}: Props<T>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
" border border-nb-gray-900/50 p-5 rounded-lg transition-all max-w-[360px] flex flex-col justify-between gap-4",
|
||||
switchState ? "bg-nb-gray-930/50" : "bg-nb-gray-930/30",
|
||||
disabled && "opacity-60 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className={"flex flex-col gap-4"}>
|
||||
<div className={"flex justify-between"}>
|
||||
<div className={"flex gap-4"}>
|
||||
<div
|
||||
className={
|
||||
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
|
||||
}
|
||||
>
|
||||
<Image src={image} alt={name} className={"rounded-[4px]"} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={""}>{name}</h3>
|
||||
<InlineLink
|
||||
href={url.href}
|
||||
target={"_blank"}
|
||||
className={"text-sm font-light"}
|
||||
variant={"faded"}
|
||||
>
|
||||
{url.title}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
{!hideSwitch && (
|
||||
<div className={"flex items-center"}>
|
||||
<ToggleSwitch
|
||||
checked={switchState}
|
||||
onCheckedChange={onEnabledChange}
|
||||
className={"grow"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Paragraph className={"text-sm font-light"}>{description}</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data == undefined ? (
|
||||
<div>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
className={"w-full items-center"}
|
||||
onClick={onSetup}
|
||||
>
|
||||
<Repeat size={13} />
|
||||
Connect {name}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ArrowRightLeft } from "lucide-react";
|
||||
import { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import netBirdLogo from "@/assets/netbird.svg";
|
||||
|
||||
type Props = {
|
||||
image: StaticImport | string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
export const IntegrationModalHeader = ({
|
||||
image,
|
||||
title,
|
||||
description,
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={"flex justify-center items-center gap-4 mt-5"}>
|
||||
<div
|
||||
className={
|
||||
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={netBirdLogo}
|
||||
alt={"NetBird"}
|
||||
className={"rounded-[4px]"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ArrowRightLeft size={24} className={"text-netbird"} />
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
|
||||
}
|
||||
>
|
||||
<Image src={image} alt={""} className={"rounded-[4px]"} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"mx-auto text-center flex flex-col items-center justify-center mt-6 z-[1]"
|
||||
}
|
||||
>
|
||||
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>{title}</h2>
|
||||
<Paragraph className={cn("text-sm text-center max-w-[450px] px-4")}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import { IconCircleFilled } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { FileText } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import datadogLogo from "@/assets/integrations/datadog.png";
|
||||
import { EventStream } from "@/interfaces/EventStream";
|
||||
|
||||
export const EventStreamingCard = () => {
|
||||
const { data: eventStreamIntegrations } = useFetchApi<EventStream[]>(
|
||||
"/integrations/event-streaming",
|
||||
);
|
||||
const dataDogSettings = eventStreamIntegrations?.find(
|
||||
(integration) => integration.platform === "datadog",
|
||||
);
|
||||
|
||||
const enabled = dataDogSettings ? dataDogSettings.enabled : false;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={"p-default pb-6"}>
|
||||
<div
|
||||
onClick={() => router.push("/integrations")}
|
||||
className={cn(
|
||||
"border cursor-pointer border-nb-gray-900/50 bg-nb-gray-900/30 hover:bg-nb-gray-900/50 py-3 pl-3 pr-5 rounded-lg transition-all min-w-[310px] max-w-[400px]",
|
||||
)}
|
||||
>
|
||||
<div className={"inline-flex gap-4 w-full"}>
|
||||
<div
|
||||
className={
|
||||
"h-10 w-10 shrink-0 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
|
||||
}
|
||||
>
|
||||
{dataDogSettings?.enabled && (
|
||||
<Image
|
||||
src={datadogLogo}
|
||||
alt={"Datadog"}
|
||||
className={"rounded-[4px]"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!dataDogSettings && <FileText size={16} />}
|
||||
</div>
|
||||
<div className={""}>
|
||||
<div className={"flex items-center gap-3 justify-between"}>
|
||||
<div className={"font-medium text-sm flex gap-2 items-center"}>
|
||||
Event Streaming
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs flex gap-2 items-center mb-2 font-medium",
|
||||
enabled ? "text-green-500" : "text-nb-gray-500",
|
||||
)}
|
||||
>
|
||||
<IconCircleFilled size={8} />
|
||||
{enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className={"text-xs font-light !text-nb-gray-300 "}>
|
||||
Stream your activity events to third-party services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { ExternalLinkIcon, FileText } from "lucide-react";
|
||||
import React from "react";
|
||||
import IntegrationIcon from "@/assets/icons/IntegrationIcon";
|
||||
import Datadog from "@/modules/integrations/event-streaming/datadog/Datadog";
|
||||
|
||||
export default function EventStreamingTab() {
|
||||
return (
|
||||
<Tabs.Content value={"event-streaming"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/integrations"}
|
||||
label={"Integrations"}
|
||||
icon={<IntegrationIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/integrations"}
|
||||
label={"Event Streaming"}
|
||||
icon={<FileText size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>Event Streaming</h1>
|
||||
<Paragraph>
|
||||
Event Streaming allows you to stream NetBirds activity events to
|
||||
different third-party services.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/activity-event-streaming"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Event Streaming
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
<div className={"gap-6 mt-6 flex flex-wrap"}>
|
||||
<Datadog />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { notify } from "@components/Notification";
|
||||
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import integrationImage from "@/assets/integrations/datadog.png";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { EventStream } from "@/interfaces/EventStream";
|
||||
import DatadogSetup from "@/modules/integrations/event-streaming/datadog/DatadogSetup";
|
||||
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
|
||||
|
||||
export default function Datadog() {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: eventStreamIntegrations, isLoading } = useFetchApi<
|
||||
EventStream[]
|
||||
>("/integrations/event-streaming");
|
||||
|
||||
const dataDogSettings = eventStreamIntegrations?.find(
|
||||
(integration) => integration.platform === "datadog",
|
||||
);
|
||||
|
||||
const integrationRequest = useApiCall<EventStream>(
|
||||
"/integrations/event-streaming",
|
||||
);
|
||||
|
||||
const [setupModal, setSetupModal] = useState(false);
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const toggleSwitch = async () => {
|
||||
if (!dataDogSettings) return setSetupModal(true);
|
||||
|
||||
const choice = await confirm({
|
||||
title: `Disconnect Datadog?`,
|
||||
description:
|
||||
"Disconnecting deletes the current configuration. You will need to start the setup process again.",
|
||||
confirmText: "Disconnect",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
});
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: "Datadog Integration",
|
||||
description: `Datadog was successfully disconnected`,
|
||||
promise: integrationRequest.del({}, "/" + dataDogSettings.id).then(() => {
|
||||
mutate("/integrations/event-streaming");
|
||||
}),
|
||||
loadingMessage: "Disconnecting integration...",
|
||||
});
|
||||
};
|
||||
|
||||
return isLoading ? (
|
||||
<>
|
||||
<SkeletonIntegration />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IntegrationCard
|
||||
name="Datadog"
|
||||
description="Datadog is a monitoring service for cloud-scale applications."
|
||||
url={{
|
||||
title: "datadoghq.com",
|
||||
href: "https://www.datadoghq.com/",
|
||||
}}
|
||||
image={integrationImage}
|
||||
data={dataDogSettings}
|
||||
switchState={!dataDogSettings ? false : dataDogSettings.enabled}
|
||||
onEnabledChange={toggleSwitch}
|
||||
onSetup={() => setSetupModal(true)}
|
||||
></IntegrationCard>
|
||||
<DatadogSetup open={setupModal} onOpenChange={setSetupModal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { CountryEURounded } from "@/assets/countries/CountryEURounded";
|
||||
import { CountryJPRounded } from "@/assets/countries/CountryJPRounded";
|
||||
import { CountryUSRounded } from "@/assets/countries/CountryUSRounded";
|
||||
|
||||
export const DatadogRegions = [
|
||||
{
|
||||
name: "Europe (EU)",
|
||||
site_url: "https://app.datadoghq.eu",
|
||||
send_logs_url: "https://http-intake.logs.datadoghq.eu/api/v2/logs",
|
||||
icon: CountryEURounded,
|
||||
},
|
||||
{
|
||||
name: "United States (US1)",
|
||||
site_url: "https://app.datadoghq.com",
|
||||
send_logs_url: "https://http-intake.logs.datadoghq.com/api/v2/logs",
|
||||
icon: CountryUSRounded,
|
||||
},
|
||||
{
|
||||
name: "United States (US3)",
|
||||
site_url: "https://us3.datadoghq.com",
|
||||
send_logs_url: "https://http-intake.logs.us3.datadoghq.com/api/v2/logs",
|
||||
icon: CountryUSRounded,
|
||||
},
|
||||
{
|
||||
name: "United States (US5)",
|
||||
site_url: "https://us5.datadoghq.com",
|
||||
send_logs_url: "https://http-intake.logs.us5.datadoghq.com/api/v2/logs",
|
||||
icon: CountryUSRounded,
|
||||
},
|
||||
{
|
||||
name: "United States (US1-FED)",
|
||||
site_url: "https://app.ddog-gov.com",
|
||||
send_logs_url: "https://http-intake.logs.ddog-gov.com/api/v2/logs",
|
||||
icon: CountryUSRounded,
|
||||
},
|
||||
{
|
||||
name: "Japan (AP1)",
|
||||
site_url: "https://ap1.datadoghq.com",
|
||||
send_logs_url: "https://http-intake.logs.ap1.datadoghq.com/api/v2/logs",
|
||||
icon: CountryJPRounded,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const DatadogApiKeysPage = "/organization-settings/api-keys";
|
||||
@@ -1,277 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import { notify } from "@components/Notification";
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import Steps from "@components/Steps";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
Globe,
|
||||
GlobeIcon,
|
||||
KeyRound,
|
||||
Repeat,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import datadogLogo from "@/assets/integrations/datadog.png";
|
||||
import { EventStream } from "@/interfaces/EventStream";
|
||||
import {
|
||||
DatadogApiKeysPage,
|
||||
DatadogRegions,
|
||||
} from "@/modules/integrations/event-streaming/datadog/DatadogRegions";
|
||||
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export default function DatadogSetup({ open, onOpenChange, onSuccess }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
<SetupContent
|
||||
onSuccess={() => {
|
||||
onOpenChange(false);
|
||||
onSuccess && onSuccess();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ModalProps = {
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export function SetupContent({ onSuccess }: ModalProps) {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const integrationRequest = useApiCall<EventStream>(
|
||||
"/integrations/event-streaming",
|
||||
);
|
||||
|
||||
const datadogRegions = DatadogRegions.map((region) => {
|
||||
return {
|
||||
label: region.name,
|
||||
value: region.send_logs_url,
|
||||
icon: region.icon,
|
||||
} as SelectOption;
|
||||
});
|
||||
|
||||
const [selectedRegion, setSelectedRegion] = useState(datadogRegions[0].value);
|
||||
|
||||
const changeRegion = (region: string) => {
|
||||
setSelectedRegion(region);
|
||||
setApiUrl(region);
|
||||
};
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiUrl, setApiUrl] = useState(datadogRegions[0].value);
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
const apiKeyEntered = apiKey.length > 0 && apiKey != "";
|
||||
const apiUrlEntered = apiUrl.length > 0 && apiUrl != "";
|
||||
const apiKeyAndUrlEntered = apiKeyEntered && apiUrlEntered;
|
||||
|
||||
const apiPageUrl =
|
||||
DatadogRegions.find((region) => region.send_logs_url == apiUrl)?.site_url +
|
||||
DatadogApiKeysPage;
|
||||
|
||||
const connect = async () => {
|
||||
notify({
|
||||
title: "Datadog Integration",
|
||||
description: `Datadog was successfully connected to NetBird.`,
|
||||
promise: integrationRequest
|
||||
.post({
|
||||
platform: "datadog",
|
||||
config: {
|
||||
api_key: apiKey,
|
||||
api_url: apiUrl,
|
||||
},
|
||||
enabled: true,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/integrations/event-streaming");
|
||||
onSuccess();
|
||||
}),
|
||||
loadingMessage: "Setting up integration...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative", step === 1 ? "max-w-md" : "max-w-lg")}
|
||||
showClose={true}
|
||||
>
|
||||
<GradientFadedBackground />
|
||||
|
||||
<IntegrationModalHeader
|
||||
image={datadogLogo}
|
||||
title={"Connect NetBird with Datadog"}
|
||||
description={
|
||||
"Start streaming your NetBird activity events to Datadog. Follow the steps below to get started."
|
||||
}
|
||||
/>
|
||||
|
||||
{step == 1 && (
|
||||
<div className={"px-8 py-3 flex flex-col mt-4 z-0"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<GlobeIcon size={16} />
|
||||
Select your Datadog region
|
||||
</p>
|
||||
<p className={"mb-3 mt-2"}>
|
||||
To identify which region you are on please check out the{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.datadoghq.com/getting_started/site/"}
|
||||
target={"_blank"}
|
||||
variant={"default"}
|
||||
className={"inline"}
|
||||
>
|
||||
Datadog Documentation.
|
||||
</InlineLink>
|
||||
</p>
|
||||
<SelectDropdown
|
||||
value={selectedRegion}
|
||||
onChange={changeRegion}
|
||||
options={datadogRegions}
|
||||
/>
|
||||
<div className={"mt-3 hidden"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Globe size={16} className={"text-nb-gray-300"} />
|
||||
</div>
|
||||
}
|
||||
placeholder={"https://http-intake.logs.datadoghq.eu/api/v2/logs"}
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"mb-3"}></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == 2 && (
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 mt-4 z-0"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<KeyRound size={16} />
|
||||
Get your Datadog API Key
|
||||
</p>
|
||||
<Steps>
|
||||
<Steps.Step step={1}>
|
||||
<p>Navigate to Datadogs API Keys page</p>
|
||||
<div className={"flex gap-4"}>
|
||||
<Link href={apiPageUrl} passHref target={"_blank"}>
|
||||
<Button variant={"primary"} size={"xs"}>
|
||||
<ExternalLinkIcon size={14} />
|
||||
API Keys
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={2}>
|
||||
<p className={"font-normal"}>
|
||||
Click{" "}
|
||||
<div
|
||||
className={
|
||||
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
|
||||
}
|
||||
>
|
||||
+ New Key
|
||||
</div>{" "}
|
||||
at the top
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={3}>
|
||||
<p className={"font-normal"}>
|
||||
Give it a descriptive name like{" "}
|
||||
<div
|
||||
className={
|
||||
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
|
||||
}
|
||||
>
|
||||
NetBird Activity Events
|
||||
</div>
|
||||
and click{" "}
|
||||
<div
|
||||
className={
|
||||
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
|
||||
}
|
||||
>
|
||||
Create Key
|
||||
</div>
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={4} line={false}>
|
||||
<p className={"font-normal"}>Enter your API-Key</p>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
<div className={"mb-4"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<KeyRound size={16} className={"text-nb-gray-300"} />
|
||||
</div>
|
||||
}
|
||||
placeholder={"1c17401cf170f7ac33dd9dcdf8040eb2"}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ModalFooter className={"items-center gap-4"}>
|
||||
{step == 1 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!apiUrlEntered}
|
||||
onClick={() => setStep(2)}
|
||||
>
|
||||
Continue
|
||||
<IconArrowRight size={16} />
|
||||
</Button>
|
||||
)}
|
||||
{step == 2 && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className={"w-full"}
|
||||
onClick={() => setStep(1)}
|
||||
>
|
||||
<IconArrowLeft size={16} />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!apiKeyAndUrlEntered}
|
||||
onClick={connect}
|
||||
>
|
||||
<Repeat size={16} />
|
||||
Connect
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import { Input } from "@components/Input";
|
||||
import { useDebounce } from "@hooks/useDebounce";
|
||||
import { Folder, MinusCircleIcon, PlusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type GroupPrefixInputProps = {
|
||||
value: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
addText?: string;
|
||||
icon?: React.ReactNode;
|
||||
text?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export function GroupPrefixInput({
|
||||
value,
|
||||
onChange,
|
||||
addText = "Add group filter",
|
||||
icon = <Folder size={14} />,
|
||||
text = "Group starts with...",
|
||||
placeholder = "e.g., NetBird_",
|
||||
}: GroupPrefixInputProps) {
|
||||
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(value);
|
||||
const prefixes = useDebounce(groupPrefixes, 100);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(prefixes);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [prefixes]);
|
||||
|
||||
const onChangeHandler = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number,
|
||||
) => {
|
||||
const newPrefixes = [...groupPrefixes];
|
||||
newPrefixes[index] = e.target.value;
|
||||
setGroupPrefixes(newPrefixes);
|
||||
};
|
||||
|
||||
const onRemoveGroupPrefix = (index: number) => {
|
||||
setGroupPrefixes((p) => {
|
||||
const newPrefixes = [...p];
|
||||
newPrefixes.splice(index, 1);
|
||||
return newPrefixes;
|
||||
});
|
||||
};
|
||||
|
||||
const onAddGroupPrefix = () => {
|
||||
setGroupPrefixes((p) => {
|
||||
const newPrefixes = [...p];
|
||||
newPrefixes.push("");
|
||||
return newPrefixes;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"mt-4"}>
|
||||
{groupPrefixes.length > 0 && (
|
||||
<div className={"flex gap-3 w-full mb-3"}>
|
||||
<div className={"flex flex-col gap-2 w-full"}>
|
||||
{groupPrefixes.map((g, i) => {
|
||||
return (
|
||||
<div className={"flex gap-2 w-full"} key={i}>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
{icon}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
}
|
||||
placeholder={placeholder}
|
||||
maxWidthClass={"w-full"}
|
||||
value={g}
|
||||
className={" !text-[13px]"}
|
||||
onKeyDown={(event) => {
|
||||
if (event.code === "Space") event.preventDefault();
|
||||
}}
|
||||
onChange={(e) => onChangeHandler(e, i)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={() => onRemoveGroupPrefix(i)}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={"dotted"}
|
||||
className={"w-full"}
|
||||
size={"sm"}
|
||||
onClick={onAddGroupPrefix}
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
{addText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user