mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58cec8fcd1 | ||
|
|
d34ae9beb2 | ||
|
|
650496f670 | ||
|
|
121778c4a6 | ||
|
|
d4102c5d04 | ||
|
|
e78c35bdbe | ||
|
|
6ebee98695 | ||
|
|
f4b28d5f40 | ||
|
|
b4b6d9295b | ||
|
|
4898742ee9 | ||
|
|
79164e9dd5 | ||
|
|
5caeab118b |
No files matched your search
@@ -12,4 +12,5 @@ jobs:
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
skip: package-lock.json,*.svg
|
||||
skip: package-lock.json,*.svg
|
||||
ignore_words_list: mappin
|
||||
@@ -5,6 +5,9 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_ENV: process.env.APP_ENV || "production",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
Generated
+440
-96
File diff suppressed because it is too large.
Load diff
+6
-3
@@ -13,7 +13,7 @@
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^5.14.0",
|
||||
"@axa-fr/react-oidc": "^7.22.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -22,8 +22,9 @@
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
@@ -37,6 +38,7 @@
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"autoprefixer": "^10",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -53,7 +55,7 @@
|
||||
"framer-motion": "^10.16.4",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.287.0",
|
||||
"lucide-react": "^0.383.0",
|
||||
"next": "13.5.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
@@ -67,6 +69,7 @@
|
||||
"react-jwt": "^1.2.0",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-virtuoso": "^4.9.0",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -23,6 +23,7 @@ import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import dayjs from "dayjs";
|
||||
@@ -66,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
const peerId = queryParameter.get("id");
|
||||
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
|
||||
return peer ? (
|
||||
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
|
||||
|
||||
useRedirect("/peers", false, !peerId);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
<PeerProvider peer={peer}>
|
||||
<PeerOverview />
|
||||
</PeerProvider>
|
||||
@@ -323,6 +327,7 @@ function PeerOverview() {
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
hideAllGroup={true}
|
||||
peer={peer}
|
||||
/>
|
||||
</FullTooltip>
|
||||
|
||||
@@ -4,13 +4,12 @@ import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense, useEffect } from "react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
@@ -21,24 +20,22 @@ export default function Peers() {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{permission?.dashboard_view === "blocked" ? (
|
||||
{permission.dashboard_view === "blocked" ? (
|
||||
<PeersBlockedView />
|
||||
) : (
|
||||
<PeersView />
|
||||
<PeersProvider>
|
||||
<PeersView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersView() {
|
||||
const { data: peers, isLoading } = useFetchApi<Peer[]>("/peers");
|
||||
const { peers, isLoading } = usePeers();
|
||||
const { users } = useUsers();
|
||||
const { refresh } = useGroups();
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
const peersWithUser = peers?.map((peer) => {
|
||||
if (!users) return peer;
|
||||
@@ -58,7 +55,7 @@ function PeersView() {
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{peers && peers.length > 1 ? `${peers.length} Peers` : "Peers"}</h1>
|
||||
<h1 ref={headingRef}>Peers</h1>
|
||||
<Paragraph>
|
||||
A list of all machines and devices connected to your private network.
|
||||
Use this view to manage peers.
|
||||
@@ -76,7 +73,11 @@ function PeersView() {
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable isLoading={isLoading} peers={peersWithUser} />
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
@@ -42,6 +43,8 @@ export default function UserPage() {
|
||||
return users?.find((u) => u.id === userId);
|
||||
}, [users, userId]);
|
||||
|
||||
useRedirect("/team/users", false, !userId);
|
||||
|
||||
return !isLoading && user ? (
|
||||
<UserOverview user={user} />
|
||||
) : (
|
||||
@@ -184,7 +187,7 @@ function UserOverview({ user }: Props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl"}>
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<UserInformationCard user={user} />
|
||||
<div className={"flex flex-col gap-8 w-1/2 "}>
|
||||
{!user.is_service_user && (
|
||||
@@ -197,6 +200,7 @@ function UserOverview({ user }: Props) {
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
hideAllGroup={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -211,6 +215,8 @@ function UserOverview({ user }: Props) {
|
||||
<UserRoleSelector
|
||||
value={role}
|
||||
onChange={setRole}
|
||||
hideOwner={user.is_service_user}
|
||||
currentUser={user}
|
||||
disabled={
|
||||
isLoggedInUser ||
|
||||
!isOwnerOrAdmin ||
|
||||
@@ -298,15 +304,18 @@ function UserInformationCard({ user }: { user: User }) {
|
||||
|
||||
{!isServiceUser && (
|
||||
<>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
{!user.is_current && user.role != Role.Owner && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
|
||||
@@ -68,4 +68,9 @@ p {
|
||||
|
||||
.stepper-bg-variant .step-circle {
|
||||
@apply !border-[#1d2024];
|
||||
}
|
||||
|
||||
.webkit-scroll{
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
}
|
||||
+33
-7
@@ -1,14 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
export default function NotFound() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push("/peers");
|
||||
});
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
return <FullScreenLoading />;
|
||||
useEffect(() => {
|
||||
setQueryParams(tempQueryParams);
|
||||
setTempQueryParams("");
|
||||
setMounted(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return mounted ? (
|
||||
<Redirect
|
||||
url={window?.location?.pathname || "/"}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
@@ -16,6 +16,8 @@ export default function CircleIcon({
|
||||
return (
|
||||
<span
|
||||
style={{ width: size + "px", height: size + "px" }}
|
||||
data-cy="circle-icon"
|
||||
data-cy-status={active ? "active" : "inactive"}
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
active
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { OidcProvider } from "@axa-fr/react-oidc";
|
||||
import {
|
||||
AuthorityConfiguration,
|
||||
OidcConfiguration,
|
||||
} from "@axa-fr/react-oidc/dist/vanilla/oidc";
|
||||
OidcProvider,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
@@ -30,7 +30,7 @@ const auth0AuthorityConfig: AuthorityConfiguration = {
|
||||
revocation_endpoint: new URL("oauth/revoke", config.authority).href,
|
||||
end_session_endpoint: new URL("v2/logout", config.authority).href,
|
||||
userinfo_endpoint: new URL("userinfo", config.authority).href,
|
||||
//issuer: new URL("", config.authority).href,
|
||||
issuer: new URL("", config.authority).href,
|
||||
};
|
||||
|
||||
const onEvent = (configurationName: any, eventName: any, data: any) => {
|
||||
|
||||
@@ -11,9 +11,17 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
const currentPath = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined = undefined;
|
||||
if (!isAuthenticated) {
|
||||
login(currentPath);
|
||||
timeout = setTimeout(async () => {
|
||||
if (!isAuthenticated) {
|
||||
await login(currentPath);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [currentPath, isAuthenticated, login]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,14 +5,16 @@ import React, { forwardRef } from "react";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ButtonGroup({ children, disabled }: Props) {
|
||||
function ButtonGroup({ children, disabled, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border-[1px] dark:border-nb-gray-900 border-neutral-200 overflow-hidden flex items-center justify-center shrink-0 border-separate",
|
||||
disabled ? "opacity-100 !border-nb-gray-900/20" : "",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -21,7 +23,10 @@ function ButtonGroup({ children, disabled }: Props) {
|
||||
}
|
||||
|
||||
const ButtonGroupButton = forwardRef(
|
||||
({ ...props }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
(
|
||||
{ className, ...props }: ButtonProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -31,6 +36,7 @@ const ButtonGroupButton = forwardRef(
|
||||
className={cn(
|
||||
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[41px]",
|
||||
"!py-2.5 !px-4",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DropdownInfoText = ({ children }: Props) => {
|
||||
return (
|
||||
<div className={"text-center pt-2 mb-6 text-nb-gray-400"}>{children}</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const DropdownInput = forwardRef<HTMLInputElement, Props>(
|
||||
({ value, onChange, placeholder = "Search..." }, ref) => {
|
||||
return (
|
||||
<div className={"relative w-full"}>
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
|
||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
|
||||
)}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className={"absolute left-0 top-0 h-full flex items-center pl-4"}>
|
||||
<div className={"flex items-center"}>
|
||||
<SearchIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={"absolute right-0 top-0 h-full flex items-center pr-4"}>
|
||||
<div
|
||||
className={
|
||||
"flex items-center bg-nb-gray-800 py-1 px-1.5 rounded-[4px] border border-nb-gray-500"
|
||||
}
|
||||
>
|
||||
<IconArrowBack size={10} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DropdownInput.displayName = "DropdownInput";
|
||||
@@ -13,6 +13,7 @@ export interface InputProps
|
||||
icon?: React.ReactNode;
|
||||
error?: string;
|
||||
errorTooltip?: boolean;
|
||||
errorTooltipPosition?: "top" | "top-right";
|
||||
}
|
||||
|
||||
const inputVariants = cva("", {
|
||||
@@ -49,6 +50,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
maxWidthClass = "",
|
||||
error,
|
||||
errorTooltip = false,
|
||||
errorTooltipPosition = "top",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -105,9 +107,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
</div>
|
||||
{error && errorTooltip && (
|
||||
<div
|
||||
className={
|
||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
|
||||
}
|
||||
className={cn(
|
||||
errorTooltipPosition == "top" &&
|
||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center",
|
||||
errorTooltipPosition == "top-right" &&
|
||||
"absolute -right-6 top-2 h-[0px] w-full flex items-center pr-3 justify-end",
|
||||
)}
|
||||
>
|
||||
<FullTooltip
|
||||
content={
|
||||
@@ -120,7 +125,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
align={"center"}
|
||||
align={errorTooltipPosition == "top" ? "center" : "end"}
|
||||
side={"top"}
|
||||
keepOpen={true}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CommandItem } from "@components/Command";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
@@ -62,8 +64,13 @@ export function NetworkRouteSelector({
|
||||
const isSearching = search.length > 0;
|
||||
const found =
|
||||
dropdownOptions.filter((item) => {
|
||||
const hasDomains = item?.domains ? item.domains.length > 0 : false;
|
||||
const domains =
|
||||
hasDomains && item?.domains ? item?.domains.join(" ") : "";
|
||||
return (
|
||||
item.network_id.includes(search) || item.network.includes(search)
|
||||
item.network_id.includes(search) ||
|
||||
item.network?.includes(search) ||
|
||||
domains.includes(search)
|
||||
);
|
||||
}).length > 0;
|
||||
return isSearching && !found;
|
||||
@@ -102,12 +109,12 @@ export function NetworkRouteSelector({
|
||||
{value ? (
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-between text-sm text-white w-full pr-4 pl-1"
|
||||
"flex items-center justify-between text-sm text-white w-full pr-4 pl-1 gap-2"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
<NetworkRoutesIcon size={16} />
|
||||
{value.network_id}
|
||||
<TextWithTooltip text={value.network_id} maxChars={15} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -117,6 +124,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{value.network}
|
||||
</div>
|
||||
<DomainList domains={value?.domains} />
|
||||
</div>
|
||||
) : (
|
||||
<span>Select an existing network...</span>
|
||||
@@ -208,15 +216,23 @@ export function NetworkRouteSelector({
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.network + option.network_id}
|
||||
value={option.network + option.network_id}
|
||||
value={
|
||||
option.network +
|
||||
option.network_id +
|
||||
option?.domains?.join(", ")
|
||||
}
|
||||
onSelect={() => {
|
||||
togglePeer(option);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={"gap-2"}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
<NetworkRoutesIcon size={14} />
|
||||
{option.network_id}
|
||||
<TextWithTooltip
|
||||
text={option.network_id}
|
||||
maxChars={15}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -226,6 +242,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{option.network}
|
||||
</div>
|
||||
<DomainList domains={option?.domains} />
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
@@ -238,3 +255,23 @@ export function NetworkRouteSelector({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainList({ domains }: { domains?: string[] }) {
|
||||
const firstDomain = domains ? domains[0] : "";
|
||||
return (
|
||||
domains &&
|
||||
domains.length > 0 && (
|
||||
<FullTooltip
|
||||
content={<div className={"text-xs max-w-sm"}>{domains.join(", ")}</div>}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"text-xs text-nb-gray-300 block min-w-0 truncate max-w-[180px]"
|
||||
}
|
||||
>
|
||||
{firstDomain} {domains.length > 1 && "+" + (domains.length - 1)}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ interface MultiSelectProps {
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
popoverWidth?: "auto" | number;
|
||||
hideAllGroup?: boolean;
|
||||
}
|
||||
export function PeerGroupSelector({
|
||||
onChange,
|
||||
@@ -37,6 +38,7 @@ export function PeerGroupSelector({
|
||||
max,
|
||||
disabled = false,
|
||||
popoverWidth = "auto",
|
||||
hideAllGroup = false,
|
||||
}: MultiSelectProps) {
|
||||
const { groups, dropdownOptions, setDropdownOptions } = useGroups();
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
@@ -47,7 +49,13 @@ export function PeerGroupSelector({
|
||||
useEffect(() => {
|
||||
if (!groups) return;
|
||||
const sortedGroups = sortBy([...groups], "name") as Group[];
|
||||
setDropdownOptions(unionBy(sortedGroups, dropdownOptions, "name"));
|
||||
|
||||
let uniqueGroups = unionBy(sortedGroups, dropdownOptions, "name");
|
||||
uniqueGroups = hideAllGroup
|
||||
? uniqueGroups.filter((group) => group.name !== "All")
|
||||
: uniqueGroups;
|
||||
|
||||
setDropdownOptions(uniqueGroups);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [groups]);
|
||||
|
||||
@@ -66,8 +74,11 @@ export function PeerGroupSelector({
|
||||
const option = dropdownOptions.find((option) => option.name == name);
|
||||
const groupPeers: GroupPeer[] | undefined =
|
||||
(group?.peers as GroupPeer[]) || [];
|
||||
groupPeers &&
|
||||
groupPeers.push({ id: peer?.id as string, name: peer?.name as string });
|
||||
|
||||
if (peer) {
|
||||
groupPeers &&
|
||||
groupPeers.push({ id: peer?.id as string, name: peer?.name as string });
|
||||
}
|
||||
|
||||
if (!group && !option) {
|
||||
setDropdownOptions((previous) => [
|
||||
@@ -100,17 +111,18 @@ export function PeerGroupSelector({
|
||||
const isSearching = search.length > 0;
|
||||
const groupDoesNotExist =
|
||||
dropdownOptions.filter((item) => item.name == trim(search)).length == 0;
|
||||
return isSearching && groupDoesNotExist;
|
||||
const isAllGroup = search.toLowerCase() == "all";
|
||||
return isSearching && groupDoesNotExist && !isAllGroup;
|
||||
}, [search, dropdownOptions]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const folderIcon = useMemo(() => {
|
||||
return <FolderGit2 size={12} />;
|
||||
return <FolderGit2 size={12} className={"shrink-0"} />;
|
||||
}, []);
|
||||
|
||||
const peerIcon = useMemo(() => {
|
||||
return <MonitorSmartphoneIcon size={14} />;
|
||||
return <MonitorSmartphoneIcon size={14} className={"shrink-0"} />;
|
||||
}, []);
|
||||
|
||||
const [slice, setSlice] = useState(10);
|
||||
@@ -198,11 +210,12 @@ export function PeerGroupSelector({
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
data-cy={"group-search-input"}
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
|
||||
"dark:placeholder:text-neutral-500 font-light placeholder:text-neutral-500 pl-10",
|
||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
|
||||
)}
|
||||
ref={searchRef}
|
||||
value={search}
|
||||
@@ -237,9 +250,7 @@ export function PeerGroupSelector({
|
||||
|
||||
<CommandGroup>
|
||||
<ScrollArea
|
||||
className={
|
||||
"max-h-[195px] overflow-y-auto flex flex-col gap-1 pl-2 py-2 pr-3"
|
||||
}
|
||||
className={"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3"}
|
||||
>
|
||||
{searchedGroupNotFound && (
|
||||
<CommandItem
|
||||
|
||||
+91
-150
@@ -1,21 +1,31 @@
|
||||
import { CommandItem } from "@components/Command";
|
||||
import { DropdownInfoText } from "@components/DropdownInfoText";
|
||||
import { DropdownInput } from "@components/DropdownInput";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
|
||||
import { useSearch } from "@hooks/useSearch";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||
import { sortBy, trim, unionBy } from "lodash";
|
||||
import { ChevronsUpDown, MapPin, SearchIcon } from "lucide-react";
|
||||
import { sortBy, unionBy } from "lodash";
|
||||
import { ChevronsUpDown, MapPin } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { FcLinux } from "react-icons/fc";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
|
||||
const MapPinIcon = memo(() => <MapPin size={12} />);
|
||||
MapPinIcon.displayName = "MapPinIcon";
|
||||
|
||||
const LinuxIcon = memo(() => (
|
||||
<span className={"grayscale brightness-[100%] contrast-[40%]"}>
|
||||
<FcLinux className={"text-white text-lg min-w-[20px] brightness-150"} />
|
||||
</span>
|
||||
));
|
||||
LinuxIcon.displayName = "LinuxIcon";
|
||||
|
||||
interface MultiSelectProps {
|
||||
value?: Peer;
|
||||
onChange: React.Dispatch<React.SetStateAction<Peer | undefined>>;
|
||||
@@ -23,6 +33,13 @@ interface MultiSelectProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const searchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
if (item.hostname.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().startsWith(lowerCaseQuery);
|
||||
};
|
||||
|
||||
export function PeerSelector({
|
||||
onChange,
|
||||
value,
|
||||
@@ -30,13 +47,16 @@ export function PeerSelector({
|
||||
disabled = false,
|
||||
}: MultiSelectProps) {
|
||||
const { data: peers } = useFetchApi<Peer[]>("/peers");
|
||||
|
||||
const [dropdownOptions, setDropdownOptions] = useState<Peer[]>([]);
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Update dropdown options when peers change
|
||||
const [unfilteredItems, setUnfilteredItems] = useState<Peer[]>([]);
|
||||
const [filteredItems, search, setSearch] = useSearch(
|
||||
unfilteredItems,
|
||||
searchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
);
|
||||
|
||||
// Update unfiltered items when peers change
|
||||
useEffect(() => {
|
||||
if (!peers) return;
|
||||
|
||||
@@ -56,7 +76,7 @@ export function PeerSelector({
|
||||
});
|
||||
}
|
||||
|
||||
setDropdownOptions(unionBy(options, dropdownOptions, "id"));
|
||||
setUnfilteredItems(unionBy(options, unfilteredItems, "id"));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [peers]);
|
||||
|
||||
@@ -68,44 +88,11 @@ export function PeerSelector({
|
||||
onChange(peer);
|
||||
setSearch("");
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const peerNotFound = useMemo(() => {
|
||||
const isSearching = search.length > 0;
|
||||
|
||||
// Search peer by ip or name
|
||||
const peerFound =
|
||||
dropdownOptions.filter((item) => {
|
||||
return (
|
||||
item.name.includes(search) ||
|
||||
item.hostname.includes(search) ||
|
||||
item.ip.includes(search)
|
||||
);
|
||||
}).length > 0;
|
||||
|
||||
return isSearching && !peerFound;
|
||||
}, [search, dropdownOptions]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [slice, setSlice] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
setSlice(dropdownOptions.length);
|
||||
}, 100);
|
||||
} else {
|
||||
setSlice(10);
|
||||
}
|
||||
}, [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}
|
||||
@@ -141,7 +128,7 @@ export function PeerSelector({
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
{LinuxIcon}
|
||||
<LinuxIcon />
|
||||
<TextWithTooltip text={value.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
@@ -150,7 +137,7 @@ export function PeerSelector({
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
||||
}
|
||||
>
|
||||
<MapPin size={12} />
|
||||
<MapPinIcon />
|
||||
{value.ip}
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,113 +155,67 @@ export function PeerSelector({
|
||||
style={{
|
||||
width: width,
|
||||
}}
|
||||
forceMount={true}
|
||||
align="start"
|
||||
side={"top"}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Command
|
||||
className={"w-full flex"}
|
||||
loop
|
||||
filter={(value, search) => {
|
||||
const formatValue = trim(value.toLowerCase());
|
||||
const formatSearch = trim(search.toLowerCase());
|
||||
if (formatValue.includes(formatSearch)) return 1;
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
|
||||
"dark:placeholder:text-neutral-500 font-light placeholder:text-neutral-500 pl-10",
|
||||
)}
|
||||
ref={searchRef}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
placeholder={"Search for peers by name or ip..."}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
"absolute left-0 top-0 h-full flex items-center pl-4"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center"}>
|
||||
<SearchIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"absolute right-0 top-0 h-full flex items-center pr-4"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex items-center bg-nb-gray-800 py-1 px-1.5 rounded-[4px] border border-nb-gray-500"
|
||||
}
|
||||
>
|
||||
<IconArrowBack size={10} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<DropdownInput
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder={"Search for peers by name or ip..."}
|
||||
/>
|
||||
|
||||
<div className={""}>
|
||||
{dropdownOptions.length == 0 && !peerNotFound && (
|
||||
<div
|
||||
className={
|
||||
"text-center pb-2 text-nb-gray-500 max-w-xs mx-auto"
|
||||
}
|
||||
>
|
||||
{
|
||||
"Seems like you don't have any linux peers to assign as a routing peer."
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{peerNotFound && (
|
||||
<div className={"text-center pb-2 text-nb-gray-500"}>
|
||||
There are no peers matching your search.
|
||||
</div>
|
||||
)}
|
||||
<CommandGroup>
|
||||
<ScrollArea
|
||||
className={
|
||||
"max-h-[180px] overflow-y-auto flex flex-col gap-1 pl-2 py-2 pr-3"
|
||||
}
|
||||
>
|
||||
{dropdownOptions.slice(0, slice).map((option) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.name}
|
||||
value={option.name + option.id}
|
||||
onSelect={() => {
|
||||
togglePeer(option);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
{LinuxIcon}
|
||||
<TextWithTooltip text={option.name} maxChars={20} />
|
||||
</div>
|
||||
{unfilteredItems.length == 0 && (
|
||||
<DropdownInfoText>
|
||||
{
|
||||
"Seems like you don't have any linux peers to assign as a routing peer."
|
||||
}
|
||||
</DropdownInfoText>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
||||
}
|
||||
>
|
||||
<MapPin size={12} />
|
||||
{option.ip}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</div>
|
||||
</CommandList>
|
||||
</Command>
|
||||
{filteredItems.length == 0 && (
|
||||
<DropdownInfoText>
|
||||
There are no peers matching your search.
|
||||
</DropdownInfoText>
|
||||
)}
|
||||
|
||||
{filteredItems.length > 0 && (
|
||||
<VirtualScrollAreaList
|
||||
items={filteredItems}
|
||||
onSelect={togglePeer}
|
||||
renderItem={(option) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 text-sm",
|
||||
value && value.id == option.id
|
||||
? "text-white"
|
||||
: "text-nb-gray-300",
|
||||
)}
|
||||
>
|
||||
<LinuxIcon />
|
||||
<TextWithTooltip text={option.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium flex items-center gap-1 font-mono text-[10px]",
|
||||
value && value.id == option.id
|
||||
? "text-white"
|
||||
: "text-nb-gray-300",
|
||||
)}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{option.ip}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
Loaded 20 of 96 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user