Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe6d8c9bd5 | ||
|
|
121976d101 | ||
|
|
f7071e00b6 | ||
|
|
6b73ccf102 | ||
|
|
87dcd00264 | ||
|
|
99f1bcc375 | ||
|
|
bf34c55110 | ||
|
|
1dfc6e2d75 | ||
|
|
b7860a8786 | ||
|
|
c9172e3a5f | ||
|
|
78d75134f9 | ||
|
|
071feb02f9 | ||
|
|
8e7bcc0c22 | ||
|
|
02a0b71e46 | ||
|
|
a8b66d935f | ||
|
|
f74f9cf812 |
1
.github/workflows/build_and_push.yml
vendored
1
.github/workflows/build_and_push.yml
vendored
@@ -2,6 +2,7 @@ name: build and push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
12
package-lock.json
generated
12
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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -26,23 +26,28 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import dayjs from "dayjs";
|
||||
import { trim } from "lodash";
|
||||
import { isEmpty, trim } from "lodash";
|
||||
import {
|
||||
Cpu,
|
||||
FlagIcon,
|
||||
Globe,
|
||||
History,
|
||||
MapPin,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
PencilIcon,
|
||||
TerminalSquare,
|
||||
} from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { toASCII } from "punycode";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { useSWRConfig } from "swr";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import CircleIcon from "@/assets/icons/CircleIcon";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
@@ -139,7 +144,7 @@ function PeerOverview() {
|
||||
<CircleIcon
|
||||
active={peer.connected}
|
||||
size={12}
|
||||
className={"mb-[3px]"}
|
||||
className={"mb-[3px] shrink-0"}
|
||||
/>
|
||||
<TextWithTooltip text={name} maxChars={30} />
|
||||
|
||||
@@ -291,10 +296,18 @@ function PeerOverview() {
|
||||
}
|
||||
|
||||
function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"NetBird IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
@@ -305,6 +318,20 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Domain name"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
@@ -313,7 +340,10 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
}
|
||||
value={peer.dns_label}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
@@ -322,6 +352,35 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
tooltip={false}
|
||||
value={
|
||||
isEmpty(peer.country_code) ? (
|
||||
"Unknown"
|
||||
) : (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton width={140} />
|
||||
) : (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<div className={"border-0 border-nb-gray-800 rounded-full"}>
|
||||
<RoundedFlag country={peer.country_code} size={12} />
|
||||
</div>
|
||||
{countryText}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
@@ -347,6 +406,7 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
")"
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
|
||||
@@ -6,17 +6,35 @@ 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 { isUser } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isUser ? <PeersDefaultView /> : <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 +45,7 @@ export default function Peers() {
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
@@ -56,6 +74,37 @@ export default function Peers() {
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable isLoading={isLoading} peers={peersWithUser} />
|
||||
</Suspense>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersDefaultView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new peer 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,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 +104,7 @@ function UserOverview({ user }: Props) {
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
disabled={isUser}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
|
||||
@@ -117,6 +118,7 @@ function UserOverview({ user }: Props) {
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={"Users"}
|
||||
disabled={isUser}
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
)}
|
||||
@@ -156,28 +158,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 +194,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];
|
||||
}
|
||||
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 |
@@ -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"}
|
||||
|
||||
@@ -30,6 +30,8 @@ type CardListItemProps = {
|
||||
value: React.ReactNode;
|
||||
className?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
};
|
||||
|
||||
function CardListItem({
|
||||
@@ -37,6 +39,8 @@ function CardListItem({
|
||||
value,
|
||||
className,
|
||||
copy = false,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
}: CardListItemProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
|
||||
@@ -54,11 +58,18 @@ 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.`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
{copy && <Copy size={13} />}
|
||||
{tooltip ? (
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -28,12 +28,16 @@ export default function CopyToClipboardText({ children, message }: Props) {
|
||||
|
||||
{copied ? (
|
||||
<CheckIcon
|
||||
className={"text-nb-gray-100 opacity-0 group-hover:opacity-100"}
|
||||
className={
|
||||
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
|
||||
}
|
||||
size={12}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={"text-nb-gray-100 opacity-0 group-hover:opacity-100"}
|
||||
className={
|
||||
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
|
||||
}
|
||||
size={12}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function FullTooltip({
|
||||
className={cn(
|
||||
isAction ? "cursor-pointer" : "cursor-default",
|
||||
"inline-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",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal file
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
export default function SkeletonPeerDetail() {
|
||||
return (
|
||||
<div className={"w-full mt-6 p-default"}>
|
||||
<div className={"flex flex-wrap w-full justify-between max-w-6xl "}>
|
||||
<Skeleton height={24} width={300} className={"rounded-md"} />
|
||||
</div>
|
||||
<div className={"flex flex-wrap w-full justify-between mt-4 max-w-6xl "}>
|
||||
<Skeleton height={42} width={400} className={"rounded-md"} />
|
||||
<div className={"flex gap-3"}>
|
||||
<Skeleton height={42} width={80} className={"rounded-md"} />
|
||||
<Skeleton height={42} width={120} className={"rounded-md"} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-wrap w-full justify-between mt-6 max-w-6xl gap-10"
|
||||
}
|
||||
>
|
||||
<Skeleton
|
||||
height={400}
|
||||
width={"100%"}
|
||||
className={"rounded-md"}
|
||||
containerClassName={"flex-1 "}
|
||||
/>
|
||||
<Skeleton
|
||||
height={300}
|
||||
width={"100%"}
|
||||
className={"rounded-md opacity-30"}
|
||||
containerClassName={"flex-1 "}
|
||||
/>
|
||||
</div>
|
||||
</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,
|
||||
@@ -223,6 +224,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 +248,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;
|
||||
}
|
||||
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;
|
||||
};
|
||||
@@ -2,19 +2,16 @@ import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { createElement, useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { Country } from "@/interfaces/Country";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
export const CountrySelector = ({ value, onChange }: Props) => {
|
||||
const { data: countries, isLoading } = useFetchApi<Country[]>(
|
||||
"/locations/countries",
|
||||
);
|
||||
const { countries, isLoading } = useCountries();
|
||||
|
||||
const countryList = useMemo(() => {
|
||||
return countries?.map((country) => {
|
||||
|
||||
@@ -27,11 +27,14 @@ export default function GroupBadge({
|
||||
className={cn("transition-all group whitespace-nowrap", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FolderGit2 size={12} />
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
<TextWithTooltip text={group.name} maxChars={20} />
|
||||
{children}
|
||||
{showX && (
|
||||
<XIcon size={12} className={"cursor-pointer group-hover:text-white"} />
|
||||
<XIcon
|
||||
size={12}
|
||||
className={"cursor-pointer group-hover:text-white shrink-0"}
|
||||
/>
|
||||
)}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function LoginExpiredBadge({ loginExpired }: Props) {
|
||||
<Tooltip delayDuration={1}>
|
||||
<TooltipTrigger>
|
||||
<Badge variant={"red"} className={"px-3"}>
|
||||
<AlertTriangle size={14} className={"mr-1"} />
|
||||
<AlertTriangle size={13} className={"mr-1"} />
|
||||
Login required
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -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,11 +24,12 @@ export default function TextWithTooltip({
|
||||
<FullTooltip
|
||||
disabled={charCount <= maxChars || hideTooltip}
|
||||
interactive={false}
|
||||
className={"truncate w-full"}
|
||||
content={
|
||||
<div className={"max-w-xs break-all whitespace-normal"}>{text}</div>
|
||||
}
|
||||
>
|
||||
<span className={cn(className)}>
|
||||
<span className={cn(className, "truncate")}>
|
||||
{charCount > maxChars ? text && `${text.slice(0, maxChars)}...` : text}
|
||||
</span>
|
||||
</FullTooltip>
|
||||
|
||||
90
src/contexts/AnnouncementProvider.tsx
Normal file
90
src/contexts/AnnouncementProvider.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AnnouncementVariant } from "@components/ui/AnnouncementBanner";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import md5 from "crypto-js/md5";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const initialAnnouncements: Announcement[] = [];
|
||||
|
||||
export interface Announcement extends AnnouncementVariant {
|
||||
tag: string;
|
||||
text: string;
|
||||
link?: string;
|
||||
linkText?: string;
|
||||
isExternal?: boolean;
|
||||
closeable: 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;
|
||||
},
|
||||
);
|
||||
|
||||
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[]>();
|
||||
|
||||
useEffect(() => {
|
||||
const initial = initialAnnouncements.map((announcement) => {
|
||||
const hash = md5(announcement.text).toString();
|
||||
const isOpen = !closedAnnouncements.some((h) => h === hash);
|
||||
return {
|
||||
...announcement,
|
||||
hash,
|
||||
isOpen,
|
||||
};
|
||||
});
|
||||
if (initial.length > 0) {
|
||||
setAnnouncements(initial);
|
||||
}
|
||||
}, [closedAnnouncements]);
|
||||
|
||||
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 }}
|
||||
>
|
||||
{children}
|
||||
</AnnouncementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAnnouncement = () => {
|
||||
return React.useContext(AnnouncementContext);
|
||||
};
|
||||
58
src/contexts/CountryProvider.tsx
Normal file
58
src/contexts/CountryProvider.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const CountryContext = React.createContext(
|
||||
{} as {
|
||||
countries: Country[] | undefined;
|
||||
isLoading: boolean;
|
||||
getRegionByPeer: (peer: Peer) => string;
|
||||
},
|
||||
);
|
||||
|
||||
export default function CountryProvider({ children }: Props) {
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
return isUser ? (
|
||||
children
|
||||
) : (
|
||||
<CountryProviderContent>{children}</CountryProviderContent>
|
||||
);
|
||||
}
|
||||
|
||||
function CountryProviderContent({ children }: Props) {
|
||||
const { data: countries, isLoading } = useFetchApi<Country[]>(
|
||||
"/locations/countries",
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
const getRegionByPeer = useCallback(
|
||||
(peer: Peer) => {
|
||||
if (!countries) return "Unknown";
|
||||
const country = countries.find(
|
||||
(c) => c.country_code === peer.country_code,
|
||||
);
|
||||
if (!country) return "Unknown";
|
||||
if (!peer.city_name) return country.country_name;
|
||||
return `${country.country_name}, ${peer.city_name}`;
|
||||
},
|
||||
[countries],
|
||||
);
|
||||
|
||||
return (
|
||||
<CountryContext.Provider value={{ countries, isLoading, getRegionByPeer }}>
|
||||
{children}
|
||||
</CountryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useCountries = () => {
|
||||
return React.useContext(CountryContext);
|
||||
};
|
||||
@@ -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 = {
|
||||
@@ -12,20 +14,38 @@ const GroupContext = React.createContext(
|
||||
refresh: () => void;
|
||||
dropdownOptions: Group[];
|
||||
setDropdownOptions: React.Dispatch<React.SetStateAction<Group[]>>;
|
||||
isLoading: boolean;
|
||||
},
|
||||
);
|
||||
|
||||
export default function GroupsProvider({ children }: Props) {
|
||||
const { data: groups, mutate } = useFetchApi<Group[]>("/groups");
|
||||
const path = usePathname();
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
return isUser && path == "/peers" ? (
|
||||
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 (
|
||||
<GroupContext.Provider
|
||||
value={{ groups, refresh, dropdownOptions, setDropdownOptions }}
|
||||
value={{
|
||||
groups,
|
||||
refresh,
|
||||
dropdownOptions,
|
||||
setDropdownOptions,
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GroupContext.Provider>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { notify } from "@components/Notification";
|
||||
import SkeletonPeerDetail from "@components/skeletons/SkeletonPeerDetail";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -27,12 +28,13 @@ const PeerContext = React.createContext(
|
||||
) => Promise<Peer>;
|
||||
openSSHDialog: () => Promise<boolean>;
|
||||
deletePeer: () => void;
|
||||
isLoading: boolean;
|
||||
},
|
||||
);
|
||||
|
||||
export default function PeerProvider({ children, peer }: Props) {
|
||||
const user = usePeerUser(peer);
|
||||
const peerGroups = usePeerGroups(peer);
|
||||
const { peerGroups, isLoading } = usePeerGroups(peer);
|
||||
const peerRequest = useApiCall<Peer>("/peers");
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
@@ -94,12 +96,22 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
return !isLoading ? (
|
||||
<PeerContext.Provider
|
||||
value={{ peer, peerGroups, user, update, openSSHDialog, deletePeer }}
|
||||
value={{
|
||||
peer,
|
||||
peerGroups,
|
||||
user,
|
||||
update,
|
||||
openSSHDialog,
|
||||
deletePeer,
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PeerContext.Provider>
|
||||
) : (
|
||||
<SkeletonPeerDetail />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,9 +120,9 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
* @param peer
|
||||
*/
|
||||
export const usePeerGroups = (peer?: Peer) => {
|
||||
const { groups } = useGroups();
|
||||
const { groups, isLoading } = useGroups();
|
||||
|
||||
return useMemo(() => {
|
||||
const peerGroups = useMemo(() => {
|
||||
if (!peer) return [];
|
||||
const peerGroups = groups?.filter((group) => {
|
||||
const foundGroup = group.peers?.find((p) => {
|
||||
@@ -121,6 +133,8 @@ export const usePeerGroups = (peer?: Peer) => {
|
||||
});
|
||||
return peerGroups || [];
|
||||
}, [groups, peer]);
|
||||
|
||||
return { peerGroups, isLoading };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,10 +13,13 @@ 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("windows"))
|
||||
return OperatingSystem.WINDOWS as const;
|
||||
return OperatingSystem.LINUX as const;
|
||||
};
|
||||
|
||||
@@ -20,4 +20,7 @@ export interface Peer {
|
||||
login_expired: boolean;
|
||||
login_expiration_enabled: boolean;
|
||||
approval_required: boolean;
|
||||
city_name: string;
|
||||
country_code: string;
|
||||
connection_ip: string;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PostureCheck {
|
||||
nb_version_check?: NetBirdVersionCheck;
|
||||
os_version_check?: OperatingSystemVersionCheck;
|
||||
geo_location_check?: GeoLocationCheck;
|
||||
peer_network_range_check?: PeerNetworkRangeCheck;
|
||||
};
|
||||
policies?: Policy[];
|
||||
active?: boolean;
|
||||
@@ -47,6 +48,11 @@ export interface GeoLocation {
|
||||
city_name: string;
|
||||
}
|
||||
|
||||
export interface PeerNetworkRangeCheck {
|
||||
ranges: string[];
|
||||
action: "allow" | "deny";
|
||||
}
|
||||
|
||||
export const windowsKernelVersions: SelectOption[] = [
|
||||
{ value: "5.0", label: "Windows 2000" },
|
||||
{ value: "5.1", label: "Windows XP" },
|
||||
|
||||
@@ -11,6 +11,7 @@ import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import OIDCProvider from "@/auth/OIDCProvider";
|
||||
import AnalyticsProvider from "@/contexts/AnalyticsProvider";
|
||||
import AnnouncementProvider from "@/contexts/AnnouncementProvider";
|
||||
import DialogProvider from "@/contexts/DialogProvider";
|
||||
import ErrorBoundaryProvider from "@/contexts/ErrorBoundary";
|
||||
import { GlobalThemeProvider } from "@/contexts/GlobalThemeProvider";
|
||||
@@ -35,9 +36,11 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
<GlobalThemeProvider>
|
||||
<ErrorBoundaryProvider>
|
||||
<OIDCProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
<AnnouncementProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</AnnouncementProvider>
|
||||
</OIDCProvider>
|
||||
</ErrorBoundaryProvider>
|
||||
</GlobalThemeProvider>
|
||||
|
||||
@@ -9,13 +9,15 @@ import { useIsSm, useIsXs } from "@utils/responsive";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { 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,7 +28,9 @@ export default function DashboardLayout({
|
||||
<ApplicationProvider>
|
||||
<UsersProvider>
|
||||
<GroupsProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
</UsersProvider>
|
||||
</ApplicationProvider>
|
||||
@@ -38,9 +42,10 @@ function DashboardPageContent({ children }: { children: React.ReactNode }) {
|
||||
const { mobileNavOpen, toggleMobileNav } = useApplicationContext();
|
||||
const isSm = useIsSm();
|
||||
const isXs = useIsXs();
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
const navOpenPageWidth = isSm ? "50%" : isXs ? "65%" : "80%";
|
||||
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
return (
|
||||
<div className={cn("flex flex-col h-screen", mobileNavOpen && "flex")}>
|
||||
{mobileNavOpen && (
|
||||
@@ -142,13 +147,14 @@ 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 />
|
||||
{!isUser && <Navigation hideOnMobile />}
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,59 +1,98 @@
|
||||
"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 { isUser } = 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",
|
||||
isUser && "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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
/**
|
||||
* Setup Key
|
||||
*/
|
||||
|
||||
if (event.activity_code == "setupkey.revoke")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -54,13 +58,6 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "dashboard.login")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m.username}</Value> logged in to the dashboard
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "setupkey.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -77,6 +74,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,6 +109,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Route
|
||||
*/
|
||||
|
||||
if (event.activity_code == "route.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -122,6 +137,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* User
|
||||
*/
|
||||
|
||||
if (event.activity_code == "user.peer.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -150,6 +169,86 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.invite")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>{" "}
|
||||
was invited.
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.group}</Value> was added to user{" "}
|
||||
<Value>{event.meta.username}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.block")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value>
|
||||
was blocked
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.unblock")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value>
|
||||
was unblocked
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.group}</Value> was removed from user{" "}
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.role.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Role <Value>{event.meta.role}</Value> was updated of user{" "}
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Service User
|
||||
*/
|
||||
|
||||
if (event.activity_code == "service.user.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service user <Value>{event.meta.name}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "service.user.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service user <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Peer
|
||||
*/
|
||||
|
||||
if (event.activity_code == "peer.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -216,6 +315,10 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Group
|
||||
*/
|
||||
|
||||
if (event.activity_code == "group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -223,6 +326,17 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Account
|
||||
*/
|
||||
|
||||
if (event.activity_code == "account.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
@@ -230,21 +344,18 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.invite")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>{" "}
|
||||
was invited.
|
||||
</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 == "user.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.group}</Value> was added to user{" "}
|
||||
<Value>{event.meta.username}</Value>
|
||||
</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 (
|
||||
@@ -267,14 +378,9 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</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>;
|
||||
/**
|
||||
* Personal Access Token
|
||||
*/
|
||||
|
||||
if (event.activity_code == "personal.access.token.create")
|
||||
return (
|
||||
@@ -292,68 +398,9 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.block")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value>
|
||||
was blocked
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.unblock")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value>
|
||||
was unblocked
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
User <Value>{event.meta.username}</Value>{" "}
|
||||
<Value>{event.meta.email}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.group}</Value> was removed from user{" "}
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.role.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Role <Value>{event.meta.role}</Value> was updated of user{" "}
|
||||
<Value>{event.meta.username}</Value> <Value>{event.meta.email}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "service.user.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service user <Value>{event.meta.name}</Value> was created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "service.user.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service user <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{event.meta.name}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
|
||||
if (event.activity_code == "integration.create") {
|
||||
if (!event.meta.platform) return "Integration created";
|
||||
@@ -385,7 +432,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,6 +452,31 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
|
||||
// TODO add activity texts
|
||||
// rule.add
|
||||
// rule.update
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
NetworkIcon,
|
||||
Server,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -70,6 +71,10 @@ 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("posture")) {
|
||||
return (
|
||||
<ShieldCheck size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HelpCircleIcon size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
|
||||
}, [groups, initial]);
|
||||
|
||||
const [selectedGroups, setSelectedGroups] = useState<Group[]>(initialGroups);
|
||||
const peerGroups = usePeerGroups(peer);
|
||||
const { peerGroups } = usePeerGroups(peer);
|
||||
|
||||
const save = async () => {
|
||||
return Promise.all(getAllGroupCalls()).then((groups) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
};
|
||||
export default function usePeerRoutes({ peer }: Props) {
|
||||
const { data: routes } = useFetchApi<Route[]>("/routes");
|
||||
const peerGroups = usePeerGroups(peer);
|
||||
const { peerGroups } = usePeerGroups(peer);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!routes) return undefined;
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
import CopyToClipboardText from "@components/CopyToClipboardText";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isEmpty } from "lodash";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { PeerAddressTooltipContent } from "@/modules/peers/PeerAddressTooltipContent";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
export default function PeerAddressCell({ peer }: Props) {
|
||||
return (
|
||||
<div className={"flex gap-4 items-center min-w-[320px] max-w-[320px]"}>
|
||||
<FullTooltip
|
||||
side={"top"}
|
||||
interactive={false}
|
||||
contentClassName={"p-0"}
|
||||
content={<PeerAddressTooltipContent peer={peer} />}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md h-8 w-8 shrink-0",
|
||||
peer.connected ? "bg-green-600" : "bg-nb-gray-800 opacity-50",
|
||||
)}
|
||||
className={
|
||||
"flex gap-4 items-center min-w-[320px] max-w-[320px] group/cell transition-all hover:bg-nb-gray-800/10 py-2 px-3 rounded-md cursor-default"
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<GlobeIcon size={14} className={"shrink-0"} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light">
|
||||
<CopyToClipboardText
|
||||
message={"DNS label has been copied to your clipboard"}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-full h-8 w-8 shrink-0 bg-nb-gray-920/80 transition-all",
|
||||
)}
|
||||
>
|
||||
<span className={"font-normal"}>
|
||||
<TextWithTooltip
|
||||
text={peer.dns_label}
|
||||
maxChars={40}
|
||||
className={"whitespace-nowrap"}
|
||||
/>
|
||||
</span>
|
||||
</CopyToClipboardText>
|
||||
<CopyToClipboardText
|
||||
message={"IP address has been copied to your clipboard"}
|
||||
>
|
||||
<span className={"dark:text-nb-gray-400 font-mono font-thin text-xs"}>
|
||||
{peer.ip}
|
||||
</span>
|
||||
</CopyToClipboardText>
|
||||
{isEmpty(peer.country_code) ? (
|
||||
<GlobeIcon size={16} className={"text-nb-gray-300"} />
|
||||
) : (
|
||||
<RoundedFlag country={peer.country_code} size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light truncate">
|
||||
<CopyToClipboardText
|
||||
message={"DNS label has been copied to your clipboard"}
|
||||
>
|
||||
<span className={"font-normal truncate"}>{peer.dns_label}</span>
|
||||
</CopyToClipboardText>
|
||||
<CopyToClipboardText
|
||||
message={"IP address has been copied to your clipboard"}
|
||||
>
|
||||
<span
|
||||
className={"dark:text-nb-gray-400 font-mono font-thin text-xs"}
|
||||
>
|
||||
{peer.ip}
|
||||
</span>
|
||||
</CopyToClipboardText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
74
src/modules/peers/PeerAddressTooltipContent.tsx
Normal file
74
src/modules/peers/PeerAddressTooltipContent.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { FlagIcon, GlobeIcon, MapPin, NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
export const PeerAddressTooltipContent = ({ peer }: Props) => {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"text-xs flex flex-col"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
icon={<MapPin size={14} />}
|
||||
label={"NetBird IP"}
|
||||
value={peer.ip}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<NetworkIcon size={14} />}
|
||||
label={"Public IP"}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<GlobeIcon size={14} />}
|
||||
label={"Domain"}
|
||||
value={peer.dns_label}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<FlagIcon size={14} />}
|
||||
label={"Region"}
|
||||
value={
|
||||
isLoading && !countryText ? <Skeleton width={100} /> : countryText
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItem = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex justify-between gap-10 border-b border-nb-gray-920 py-2 px-4 last:border-b-0"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className={"text-nb-gray-400"}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export function PeerOSCell({ os }: { os: string }) {
|
||||
|
||||
export function OSLogo({ os }: { os: string }) {
|
||||
const icon = useMemo(() => {
|
||||
return getOperatingSystem(os.toLowerCase());
|
||||
return getOperatingSystem(os);
|
||||
}, [os]);
|
||||
|
||||
if (icon === OperatingSystem.WINDOWS)
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { ModalClose, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
|
||||
import cidr from "ip-cidr";
|
||||
import { isEmpty, uniqueId } from "lodash";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
MinusCircleIcon,
|
||||
NetworkIcon,
|
||||
PlusCircle,
|
||||
ShieldCheck,
|
||||
ShieldXIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: PeerNetworkRangeCheck;
|
||||
onChange: (value: PeerNetworkRangeCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckPeerNetworkRange = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
key={open ? 1 : 0}
|
||||
icon={<NetworkIcon size={16} />}
|
||||
title={"Peer Network Range"}
|
||||
modalWidthClass={"max-w-xl"}
|
||||
description={
|
||||
"Restrict access by allowing or blocking peer network ranges."
|
||||
}
|
||||
iconClass={"bg-gradient-to-tr from-blue-500 to-blue-400"}
|
||||
active={value !== undefined}
|
||||
onReset={() => onChange(undefined)}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
interface NetworkRange {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [allowOrDeny, setAllowOrDeny] = useState<string>(
|
||||
value?.action ? value.action : "allow",
|
||||
);
|
||||
|
||||
const [networkRanges, setNetworkRanges] = useState<NetworkRange[]>(
|
||||
value?.ranges
|
||||
? value.ranges.map((r) => {
|
||||
return {
|
||||
id: uniqueId("range"),
|
||||
value: r,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
);
|
||||
|
||||
const handleNetworkRangeChange = (id: string, value: string) => {
|
||||
const newRanges = networkRanges.map((r) =>
|
||||
r.id === id ? { ...r, value } : r,
|
||||
);
|
||||
setNetworkRanges(newRanges);
|
||||
};
|
||||
|
||||
const removeNetworkRange = (id: string) => {
|
||||
const newRanges = networkRanges.filter((r) => r.id !== id);
|
||||
setNetworkRanges(newRanges);
|
||||
};
|
||||
|
||||
const addNetworkRange = () => {
|
||||
setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]);
|
||||
};
|
||||
|
||||
const validateNetworkRange = (networkRange: string) => {
|
||||
if (networkRange == "") return "";
|
||||
const validCIDR = cidr.isValidAddress(networkRange);
|
||||
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
|
||||
return "";
|
||||
};
|
||||
|
||||
const cidrErrors = useMemo(() => {
|
||||
if (networkRanges && networkRanges.length > 0) {
|
||||
return networkRanges.map((r) => {
|
||||
return {
|
||||
id: r.id,
|
||||
error: validateNetworkRange(r.value),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [networkRanges]);
|
||||
|
||||
const hasErrorsOrIsEmpty = useMemo(() => {
|
||||
if (networkRanges.length === 0) return true;
|
||||
return cidrErrors.some((e) => e.error !== "");
|
||||
}, [networkRanges, cidrErrors]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex flex-col px-8 gap-2 pb-6"}>
|
||||
<div className={"flex justify-between items-start gap-10 mt-2"}>
|
||||
<div>
|
||||
<Label>Allow or Block Ranges</Label>
|
||||
<HelpText className={""}>
|
||||
Choose whether you want to allow or block specific peer network
|
||||
ranges
|
||||
</HelpText>
|
||||
</div>
|
||||
<RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}>
|
||||
<RadioGroupItem value={"allow"} variant={"green"}>
|
||||
<ShieldCheck size={16} />
|
||||
Allow
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value={"deny"} variant={"red"}>
|
||||
<ShieldXIcon size={16} />
|
||||
Block
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{networkRanges.length > 0 && (
|
||||
<div className={"mb-2 flex flex-col gap-2 w-full "}>
|
||||
{networkRanges.map((ipRange) => {
|
||||
return (
|
||||
<div key={ipRange.id} className={"flex gap-2"}>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={<NetworkIcon size={16} />}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
value={ipRange.value}
|
||||
error={cidrErrors.find((e) => e.id === ipRange.id)?.error}
|
||||
errorTooltip={false}
|
||||
className={"font-mono !text-[13px] w-full"}
|
||||
onChange={(e) =>
|
||||
handleNetworkRangeChange(ipRange.id, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={() => removeNetworkRange(ipRange.id)}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button variant={"dotted"} size={"sm"} onClick={addNetworkRange}>
|
||||
<PlusCircle size={16} />
|
||||
Add Network Range
|
||||
</Button>
|
||||
</div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-posture-checks#peer-network-range-check"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Peer Network Range Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={hasErrorsOrIsEmpty}
|
||||
onClick={() => {
|
||||
if (isEmpty(networkRanges)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange({
|
||||
action: allowOrDeny as "allow" | "deny",
|
||||
ranges: networkRanges
|
||||
.map((r) => r.value)
|
||||
.filter((r) => r !== ""),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
check?: PeerNetworkRangeCheck;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const PeerNetworkRangeTooltip = ({ check, children }: Props) => {
|
||||
return check ? (
|
||||
<FullTooltip
|
||||
className={"w-full"}
|
||||
interactive={true}
|
||||
contentClassName={"p-0"}
|
||||
content={
|
||||
<div
|
||||
className={"text-neutral-300 text-sm max-w-xs flex flex-col gap-1"}
|
||||
>
|
||||
<div className={"px-4 pt-3"}>
|
||||
{check.action == "allow" ? (
|
||||
<span>
|
||||
<span className={"text-green-500 font-semibold"}>
|
||||
Allow only
|
||||
</span>{" "}
|
||||
the following peer network ranges
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className={"text-red-500 font-semibold"}>Block</span> the
|
||||
following peer network ranges
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
className={"max-h-[275px] overflow-y-auto flex flex-col px-4"}
|
||||
>
|
||||
<div className={"flex flex-col gap-1.5 mt-1 text-xs mb-3.5"}>
|
||||
{check.ranges.map((ipRange, index) => {
|
||||
return (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
key={index}
|
||||
className={
|
||||
"justify-start font-medium font-mono text-[11px]"
|
||||
}
|
||||
>
|
||||
<NetworkIcon size={10} />
|
||||
{ipRange}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { PostureCheckGeoLocation } from "@/modules/posture-checks/checks/PostureCheckGeoLocation";
|
||||
import { PostureCheckNetBirdVersion } from "@/modules/posture-checks/checks/PostureCheckNetBirdVersion";
|
||||
import { PostureCheckOperatingSystem } from "@/modules/posture-checks/checks/PostureCheckOperatingSystem";
|
||||
import { PostureCheckPeerNetworkRange } from "@/modules/posture-checks/checks/PostureCheckPeerNetworkRange";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -54,6 +55,9 @@ export default function PostureCheckModal({
|
||||
const [osVersionCheck, setOsVersionCheck] = useState(
|
||||
postureCheck?.checks.os_version_check || undefined,
|
||||
);
|
||||
const [peerNetworkRangeCheck, setPeerNetworkRangeCheck] = useState(
|
||||
postureCheck?.checks.peer_network_range_check || undefined,
|
||||
);
|
||||
|
||||
const validateOSCheck = (osCheck?: OperatingSystemVersionCheck) => {
|
||||
if (!osCheck) return;
|
||||
@@ -93,6 +97,7 @@ export default function PostureCheckModal({
|
||||
nb_version_check: nbVersionCheck,
|
||||
geo_location_check: validateLocationCheck(geoLocationCheck),
|
||||
os_version_check: validateOSCheck(osVersionCheck),
|
||||
peer_network_range_check: peerNetworkRangeCheck,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -125,7 +130,10 @@ export default function PostureCheckModal({
|
||||
};
|
||||
|
||||
const isAtLeastOneCheckEnabled =
|
||||
!!nbVersionCheck || !!geoLocationCheck || !!osVersionCheck;
|
||||
!!nbVersionCheck ||
|
||||
!!geoLocationCheck ||
|
||||
!!osVersionCheck ||
|
||||
!!peerNetworkRangeCheck;
|
||||
const canCreate = !isEmpty(name) && isAtLeastOneCheckEnabled;
|
||||
|
||||
const [tab, setTab] = useState("checks");
|
||||
@@ -180,6 +188,10 @@ export default function PostureCheckModal({
|
||||
value={osVersionCheck}
|
||||
onChange={setOsVersionCheck}
|
||||
/>
|
||||
<PostureCheckPeerNetworkRange
|
||||
value={peerNetworkRangeCheck}
|
||||
onChange={setPeerNetworkRangeCheck}
|
||||
/>
|
||||
</>
|
||||
</TabsContent>
|
||||
<TabsContent value={"general"} className={"pb-8 px-8"}>
|
||||
@@ -221,9 +233,7 @@ export default function PostureCheckModal({
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-posture-checks"
|
||||
}
|
||||
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Posture Checks
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Disc3Icon, FlagIcon } from "lucide-react";
|
||||
import { Disc3Icon, FlagIcon, NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { GeoLocationTooltip } from "@/modules/posture-checks/checks/tooltips/GeoLocationTooltip";
|
||||
import { NetBirdVersionTooltip } from "@/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip";
|
||||
import { OperatingSystemTooltip } from "@/modules/posture-checks/checks/tooltips/OperatingSystemTooltip";
|
||||
import { PeerNetworkRangeTooltip } from "@/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
@@ -56,6 +57,20 @@ export const PostureCheckChecksCell = ({ check }: Props) => {
|
||||
</div>
|
||||
</OperatingSystemTooltip>
|
||||
)}
|
||||
|
||||
{check.checks.peer_network_range_check && (
|
||||
<PeerNetworkRangeTooltip
|
||||
check={check.checks.peer_network_range_check}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gradient-to-tr from-blue-500 to-blue-400 h-8 w-8 rounded-full flex items-center justify-center relative z-[8] hover:scale-[1.1] transition-all",
|
||||
)}
|
||||
>
|
||||
<NetworkIcon size={14} />
|
||||
</div>
|
||||
</PeerNetworkRangeTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@components/Select";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cn, isInt } from "@utils/helpers";
|
||||
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
import { CalendarClock, ShieldIcon, TimerReset, VoteIcon } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
@@ -55,16 +55,21 @@ export default function AuthenticationTab({ account }: Props) {
|
||||
const [expiresInSeconds] = useState(
|
||||
account.settings.peer_login_expiration || 86400,
|
||||
);
|
||||
|
||||
const [expiresIn, setExpiresIn] = useState(() => {
|
||||
if (expiresInSeconds <= 86400) return "1";
|
||||
return Math.round(expiresInSeconds / 86400).toString();
|
||||
if (expiresInSeconds <= 172800) {
|
||||
const hours = expiresInSeconds / 3600;
|
||||
return isInt(hours) ? hours.toString() : hours.toFixed(2).toString();
|
||||
}
|
||||
const days = expiresInSeconds / 86400;
|
||||
return isInt(days) ? days.toString() : days.toFixed(2).toString();
|
||||
});
|
||||
|
||||
/**
|
||||
* Interval
|
||||
*/
|
||||
const initialInterval = useMemo(() => {
|
||||
if (Number(expiresIn) <= 86400) return "hours";
|
||||
if (expiresInSeconds <= 172800) return "hours";
|
||||
return "days";
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function GroupsTab({ account }: Props) {
|
||||
* JWT Group Sync
|
||||
*/
|
||||
const [jwtGroupSync, setJwtGroupSync] = useState<boolean>(
|
||||
account.settings.groups_propagation_enabled,
|
||||
account.settings.jwt_groups_enabled,
|
||||
);
|
||||
const [jwtGroupsClaimName, setJwtGroupsClaimName] = useState(
|
||||
account.settings.jwt_groups_claim_name,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Code from "@components/Code";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
@@ -39,11 +37,12 @@ import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
const copyMessage = "Setup-Key was copied to your clipboard!";
|
||||
export default function SetupKeyModal({ children }: Props) {
|
||||
const [modal, setModal] = useState(false);
|
||||
export default function SetupKeyModal({ children, open, setOpen }: Props) {
|
||||
const [successModal, setSuccessModal] = useState(false);
|
||||
const [setupKey, setSetupKey] = useState<SetupKey>();
|
||||
const [, copy] = useCopyToClipboard(setupKey?.key);
|
||||
@@ -55,15 +54,15 @@ export default function SetupKeyModal({ children }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={modal} onOpenChange={setModal} key={modal ? 1 : 0}>
|
||||
<ModalTrigger asChild>{children}</ModalTrigger>
|
||||
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
|
||||
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
|
||||
<SetupKeyModalContent onSuccess={handleSuccess} />
|
||||
</Modal>
|
||||
<Modal
|
||||
open={successModal}
|
||||
onOpenChange={(open) => {
|
||||
setSuccessModal(open);
|
||||
setModal(open);
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
|
||||
@@ -9,7 +9,7 @@ import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { SortingState } from "@tanstack/react-table";
|
||||
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
@@ -47,114 +47,126 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Setup Keys"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={SetupKeysTableColumns}
|
||||
data={setupKeys}
|
||||
searchPlaceholder={"Search by name, type or group..."}
|
||||
columnVisibility={{
|
||||
valid: false,
|
||||
group_strings: false,
|
||||
}}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Create Setup Key"}
|
||||
description={
|
||||
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
|
||||
}
|
||||
button={
|
||||
<SetupKeyModal>
|
||||
<Button variant={"primary"} className={""}>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
</SetupKeyModal>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
<>
|
||||
<SetupKeyModal open={open} setOpen={setOpen} />
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Setup Keys"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={SetupKeysTableColumns}
|
||||
data={setupKeys}
|
||||
searchPlaceholder={"Search by name, type or group..."}
|
||||
columnVisibility={{
|
||||
valid: false,
|
||||
group_strings: false,
|
||||
}}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={
|
||||
<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />
|
||||
}
|
||||
target={"_blank"}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Create Setup Key"}
|
||||
description={
|
||||
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={""}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Setup Keys
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{setupKeys && setupKeys?.length > 0 && (
|
||||
<SetupKeyModal>
|
||||
<Button variant={"primary"} className={"ml-auto"}>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
</SetupKeyModal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<ButtonGroup disabled={setupKeys?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("valid")?.setFilterValue(true);
|
||||
}}
|
||||
disabled={setupKeys?.length == 0}
|
||||
variant={
|
||||
table.getColumn("valid")?.getFilterValue() == true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Valid
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("valid")?.setFilterValue("");
|
||||
}}
|
||||
disabled={setupKeys?.length == 0}
|
||||
variant={
|
||||
table.getColumn("valid")?.getFilterValue() != true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={setupKeys?.length == 0}
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Setup Keys
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={setupKeys?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/setup-keys").then();
|
||||
mutate("/groups").then();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{setupKeys && setupKeys?.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<ButtonGroup disabled={setupKeys?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("valid")?.setFilterValue(true);
|
||||
}}
|
||||
disabled={setupKeys?.length == 0}
|
||||
variant={
|
||||
table.getColumn("valid")?.getFilterValue() == true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Valid
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("valid")?.setFilterValue("");
|
||||
}}
|
||||
disabled={setupKeys?.length == 0}
|
||||
variant={
|
||||
table.getColumn("valid")?.getFilterValue() != true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={setupKeys?.length == 0}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={setupKeys?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/setup-keys").then();
|
||||
mutate("/groups").then();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SmallParagraph from "@components/SmallParagraph";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import AndroidIcon from "@/assets/icons/AndroidIcon";
|
||||
import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
@@ -32,29 +33,49 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function SetupModal({ showClose = true, user }: Props) {
|
||||
const os = useOperatingSystem();
|
||||
return (
|
||||
<ModalContent showClose={showClose}>
|
||||
<SetupModalContent user={user} />
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function SetupModalContent({
|
||||
user,
|
||||
header = true,
|
||||
footer = true,
|
||||
tabAlignment = "center",
|
||||
}: {
|
||||
user?: OidcUserInfo;
|
||||
header?: boolean;
|
||||
footer?: boolean;
|
||||
tabAlignment?: "center" | "start" | "end";
|
||||
}) {
|
||||
const os = useOperatingSystem();
|
||||
const [isFirstRun] = useLocalStorage<boolean>("netbird-first-run", true);
|
||||
|
||||
return (
|
||||
<ModalContent showClose={showClose}>
|
||||
<div className={"text-center pb-8 pt-4 px-8"}>
|
||||
<h2 className={"text-3xl max-w-lg mx-auto"}>
|
||||
{isFirstRun ? (
|
||||
<>
|
||||
Hello {user?.given_name || "there"}! 👋 <br />
|
||||
{`It's time to add your first device.`}
|
||||
</>
|
||||
) : (
|
||||
<>Add new peer</>
|
||||
)}
|
||||
</h2>
|
||||
<Paragraph className={"max-w-xs mx-auto mt-3"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<>
|
||||
{header && (
|
||||
<div className={"text-center pb-8 pt-4 px-8"}>
|
||||
<h2 className={"text-3xl max-w-lg mx-auto"}>
|
||||
{isFirstRun ? (
|
||||
<>
|
||||
Hello {user?.given_name || "there"}! 👋 <br />
|
||||
{`It's time to add your first device.`}
|
||||
</>
|
||||
) : (
|
||||
<>Add new peer</>
|
||||
)}
|
||||
</h2>
|
||||
<Paragraph className={"max-w-xs mx-auto mt-3"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue={String(os)}>
|
||||
<TabsList>
|
||||
<TabsList justify={tabAlignment} className={"pt-2 px-3"}>
|
||||
<TabsTrigger value={String(OperatingSystem.LINUX)}>
|
||||
<ShellIcon
|
||||
className={
|
||||
@@ -111,23 +132,26 @@ export default function SetupModal({ showClose = true, user }: Props) {
|
||||
<IOSTab />
|
||||
<DockerTab />
|
||||
</Tabs>
|
||||
<ModalFooter variant={"setup"}>
|
||||
<div>
|
||||
<SmallParagraph>
|
||||
After that you should be connected. Add more devices to your network
|
||||
or manage your existing devices in the admin panel. If you have
|
||||
further questions check out our{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/getting-started#installation"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
installation guide.
|
||||
</InlineLink>
|
||||
</SmallParagraph>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
{footer && (
|
||||
<ModalFooter variant={"setup"}>
|
||||
<div>
|
||||
<SmallParagraph>
|
||||
After that you should be connected. Add more devices to your
|
||||
network or manage your existing devices in the admin panel. 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>
|
||||
</SmallParagraph>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,11 @@ export function useNetBirdFetch(ignoreError: boolean = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function useFetchApi<T>(url: string, ignoreError = false) {
|
||||
export default function useFetchApi<T>(
|
||||
url: string,
|
||||
ignoreError = false,
|
||||
revalidate = true,
|
||||
) {
|
||||
const { fetch } = useNetBirdFetch(ignoreError);
|
||||
const handleErrors = useApiErrorHandling(ignoreError);
|
||||
|
||||
@@ -90,6 +94,9 @@ export default function useFetchApi<T>(url: string, ignoreError = false) {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
revalidateOnFocus: revalidate,
|
||||
revalidateIfStale: revalidate,
|
||||
revalidateOnReconnect: revalidate,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -75,3 +75,7 @@ export const validator = {
|
||||
return semverRegex.test(version);
|
||||
},
|
||||
};
|
||||
|
||||
export function isInt(n: number) {
|
||||
return n % 1 === 0;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ const config: Config = {
|
||||
"940": "#1b1f22",
|
||||
"950": "#181a1d",
|
||||
},
|
||||
|
||||
netbird: {
|
||||
DEFAULT: "#f68330",
|
||||
"50": "#fff6ed",
|
||||
@@ -42,6 +41,20 @@ const config: Config = {
|
||||
"900": "#7a2b14",
|
||||
"950": "#421308",
|
||||
},
|
||||
"nb-blue": {
|
||||
DEFAULT: "#31e4f5",
|
||||
"50": "#ebffff",
|
||||
"100": "#cefdff",
|
||||
"200": "#a2f9ff",
|
||||
"300": "#63f2fd",
|
||||
"400": "#31e4f5",
|
||||
"500": "#00c4da",
|
||||
"600": "#039cb7",
|
||||
"700": "#0a7c94",
|
||||
"800": "#126478",
|
||||
"900": "#145365",
|
||||
"950": "#063746",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
|
||||
Reference in New Issue
Block a user