mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Add region and public ip to peer table and detailed peer view (#340)
* Fix group badge icon size * Fix copy icon size * Add region information to peer table and single peer view * Push to docker * Change login expired icon size * Fix country flag in single peer view * Change country flag size in peer table * Disable revalidation for countries * Fix icon size on peer detail view * Rollback workflow * Revert login expiration --------- Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
This commit is contained in:
@@ -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,6 +296,12 @@ function PeerOverview() {
|
||||
}
|
||||
|
||||
function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.List>
|
||||
@@ -304,6 +315,44 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
value={peer.ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<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 +396,7 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
")"
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
|
||||
@@ -30,6 +30,7 @@ type CardListItemProps = {
|
||||
value: React.ReactNode;
|
||||
className?: string;
|
||||
copy?: boolean;
|
||||
tooltip?: boolean;
|
||||
};
|
||||
|
||||
function CardListItem({
|
||||
@@ -37,6 +38,7 @@ function CardListItem({
|
||||
value,
|
||||
className,
|
||||
copy = false,
|
||||
tooltip = true,
|
||||
}: CardListItemProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
|
||||
@@ -57,7 +59,11 @@ function CardListItem({
|
||||
copy && copyToClipBoard(`${label} has been copied to clipboard.`)
|
||||
}
|
||||
>
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
{tooltip ? (
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
{copy && <Copy size={13} />}
|
||||
</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}
|
||||
|
||||
@@ -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,7 +27,7 @@ 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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
47
src/contexts/CountryProvider.tsx
Normal file
47
src/contexts/CountryProvider.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import React, { useCallback } from "react";
|
||||
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 { 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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import React from "react";
|
||||
import ApplicationProvider, {
|
||||
useApplicationContext,
|
||||
} from "@/contexts/ApplicationProvider";
|
||||
import CountryProvider from "@/contexts/CountryProvider";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import UsersProvider from "@/contexts/UsersProvider";
|
||||
import Navigation from "@/layouts/Navigation";
|
||||
@@ -26,7 +27,9 @@ export default function DashboardLayout({
|
||||
<ApplicationProvider>
|
||||
<UsersProvider>
|
||||
<GroupsProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
</UsersProvider>
|
||||
</ApplicationProvider>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user