mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-29 20:19:56 +00:00
update peers ui
This commit is contained in:
@@ -36,7 +36,7 @@ export const Navigation = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={"wails-no-draggable shrink-0 flex items-stretch"}>
|
||||
<div className={"wails-no-draggable shrink-0 flex items-stretch "}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.value === section;
|
||||
const isDisabled = !isConnected && !isActive;
|
||||
@@ -49,7 +49,7 @@ export const Navigation = () => {
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"group relative flex flex-1 items-center justify-center",
|
||||
"gap-2.5 px-5 py-3",
|
||||
"gap-2.5 px-5 py-3.5",
|
||||
"outline-none transition-all",
|
||||
isActive ? "text-netbird" : "text-nb-gray-400 hover:text-nb-gray-300",
|
||||
isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
||||
|
||||
@@ -7,12 +7,6 @@ import { SearchInput } from "@/components/SearchInput";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { NoResults } from "@/components/NoResults";
|
||||
import { useStatus } from "@/modules/daemon-status/StatusContext";
|
||||
import {
|
||||
formatShortcut,
|
||||
useKeyboardShortcut,
|
||||
} from "@/lib/useKeyboardShortcut";
|
||||
|
||||
const SEARCH_SHORTCUT = { key: "k", cmd: true } as const;
|
||||
import { useNetworks } from "@/modules/networks/NetworksContext";
|
||||
import { ExitNodesList } from "./ExitNodesList";
|
||||
|
||||
@@ -28,11 +22,6 @@ export const ExitNodes = () => {
|
||||
searchRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useKeyboardShortcut(SEARCH_SHORTCUT, () => {
|
||||
searchRef.current?.focus();
|
||||
searchRef.current?.select();
|
||||
});
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
const matches = exitNodes.filter((r) => {
|
||||
@@ -64,28 +53,23 @@ export const ExitNodes = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4"}>
|
||||
<div className={"flex flex-col gap-3 px-6"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("exitNodes.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
shortcut={formatShortcut(SEARCH_SHORTCUT)}
|
||||
/>
|
||||
<div className={"flex flex-col w-full h-full min-h-0"}>
|
||||
<div className={"flex items-center gap-2 px-6 py-2.5 border-b border-nb-gray-910"}>
|
||||
<div className={"flex-1 min-w-0"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("exitNodes.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"auto"}
|
||||
className={"flex-1 min-h-0 overflow-hidden mt-3"}
|
||||
>
|
||||
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}>
|
||||
<ScrollArea.Viewport className={"h-full w-full"}>
|
||||
{filtered.length === 0 ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<ExitNodesList
|
||||
data={filtered}
|
||||
onToggle={toggleExitNode}
|
||||
/>
|
||||
<ExitNodesList data={filtered} onToggle={toggleExitNode} />
|
||||
)}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ChevronDown, ListFilter } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SwitchItem } from "@/components/SwitchItem";
|
||||
import { SwitchItemGroup } from "@/components/SwitchItemGroup";
|
||||
import { cn } from "@/lib/cn";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/DropdownMenu";
|
||||
|
||||
export type NetworkFilter = "all" | "active" | "overlapping";
|
||||
|
||||
@@ -13,26 +20,74 @@ type Props = {
|
||||
|
||||
export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const filters: { value: NetworkFilter; label: string }[] = [
|
||||
{ value: "all", label: t("networks.filter.all") },
|
||||
{ value: "active", label: t("networks.filter.active") },
|
||||
{ value: "overlapping", label: t("networks.filter.overlapping") },
|
||||
];
|
||||
const active = filters.find((f) => f.value === value) ?? filters[0];
|
||||
|
||||
const handleSelect = (v: NetworkFilter) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchItemGroup
|
||||
key={i18n.language}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v as NetworkFilter)}
|
||||
disabled={disabled}
|
||||
className={"w-full"}
|
||||
>
|
||||
{filters.map((f) => (
|
||||
<SwitchItem key={f.value} value={f.value} className={"flex-1"}>
|
||||
{f.label}
|
||||
<span className={"tabular-nums"}>({counts[f.value]})</span>
|
||||
</SwitchItem>
|
||||
))}
|
||||
</SwitchItemGroup>
|
||||
<DropdownMenu key={i18n.language} open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 h-9 px-2 rounded-md",
|
||||
"text-sm text-nb-gray-100",
|
||||
"outline-none hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900 transition-colors duration-150",
|
||||
"disabled:opacity-50 disabled:pointer-events-none",
|
||||
"wails-no-draggable",
|
||||
)}
|
||||
>
|
||||
<ListFilter size={14} className={"shrink-0"} />
|
||||
<span>
|
||||
{active.label}{" "}
|
||||
<span className={"tabular-nums"}>
|
||||
({counts[active.value]})
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={"text-nb-gray-400 ml-0.5 shrink-0"}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={"end"} className={"min-w-[10rem]"}>
|
||||
{filters.map((f) => {
|
||||
const checked = f.value === value;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={f.value}
|
||||
onClick={() => handleSelect(f.value)}
|
||||
className={"gap-2"}
|
||||
>
|
||||
<span className={"flex-1 truncate"}>
|
||||
{f.label}{" "}
|
||||
<span className={"tabular-nums"}>
|
||||
({counts[f.value]})
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"w-4 shrink-0 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
{checked && (
|
||||
<CheckIcon
|
||||
size={14}
|
||||
className={"text-netbird"}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,12 +7,6 @@ import { SearchInput } from "@/components/SearchInput";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { NoResults } from "@/components/NoResults";
|
||||
import { useStatus } from "@/modules/daemon-status/StatusContext";
|
||||
import {
|
||||
formatShortcut,
|
||||
useKeyboardShortcut,
|
||||
} from "@/lib/useKeyboardShortcut";
|
||||
|
||||
const SEARCH_SHORTCUT = { key: "k", cmd: true } as const;
|
||||
import { NetworkFilter, NetworkFilters } from "./NetworkFilters";
|
||||
import { NetworksList } from "./NetworksList";
|
||||
import { useNetworks } from "./NetworksContext";
|
||||
@@ -50,15 +44,7 @@ export const Networks = () => {
|
||||
searchRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useKeyboardShortcut(SEARCH_SHORTCUT, () => {
|
||||
searchRef.current?.focus();
|
||||
searchRef.current?.select();
|
||||
});
|
||||
|
||||
const overlapGroups = useMemo(
|
||||
() => buildOverlapMap(networkRoutes),
|
||||
[networkRoutes],
|
||||
);
|
||||
const overlapGroups = useMemo(() => buildOverlapMap(networkRoutes), [networkRoutes]);
|
||||
|
||||
const overlapById = useMemo(() => {
|
||||
const map = new Map<string, string[]>();
|
||||
@@ -83,9 +69,7 @@ export const Networks = () => {
|
||||
if (filter === "active" && !r.selected) return false;
|
||||
if (filter === "overlapping" && !overlapById.has(r.id)) return false;
|
||||
if (q) {
|
||||
const haystack = [r.id, r.range, ...r.domains]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
const haystack = [r.id, r.range, ...r.domains].join(" ").toLowerCase();
|
||||
if (!haystack.includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -115,33 +99,24 @@ export const Networks = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4"}>
|
||||
<div className={"flex flex-col gap-3 px-6"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("networks.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
shortcut={formatShortcut(SEARCH_SHORTCUT)}
|
||||
/>
|
||||
<NetworkFilters
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
counts={counts}
|
||||
/>
|
||||
<div className={"flex flex-col w-full h-full min-h-0"}>
|
||||
<div className={"flex items-center gap-2 px-6 py-2.5 border-b border-nb-gray-910"}>
|
||||
<div className={"flex-1 min-w-0"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("networks.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<NetworkFilters value={filter} onChange={setFilter} counts={counts} />
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"auto"}
|
||||
className={"flex-1 min-h-0 overflow-hidden mt-3"}
|
||||
>
|
||||
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}>
|
||||
<ScrollArea.Viewport className={"h-full w-full"}>
|
||||
{filtered.length === 0 ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<NetworksList
|
||||
data={filtered}
|
||||
onToggle={toggleNetwork}
|
||||
/>
|
||||
<NetworksList data={filtered} onToggle={toggleNetwork} />
|
||||
)}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ChevronDown, ListFilter } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SwitchItem } from "@/components/SwitchItem";
|
||||
import { SwitchItemGroup } from "@/components/SwitchItemGroup";
|
||||
import { cn } from "@/lib/cn";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/DropdownMenu";
|
||||
|
||||
export type StatusFilter = "all" | "online" | "offline";
|
||||
|
||||
@@ -13,26 +20,74 @@ type Props = {
|
||||
|
||||
export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const filters: { value: StatusFilter; label: string }[] = [
|
||||
{ value: "all", label: t("peers.filter.all") },
|
||||
{ value: "online", label: t("peers.filter.online") },
|
||||
{ value: "offline", label: t("peers.filter.offline") },
|
||||
];
|
||||
const active = filters.find((f) => f.value === value) ?? filters[0];
|
||||
|
||||
const handleSelect = (v: StatusFilter) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchItemGroup
|
||||
key={i18n.language}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v as StatusFilter)}
|
||||
disabled={disabled}
|
||||
className={"w-full"}
|
||||
>
|
||||
{filters.map((f) => (
|
||||
<SwitchItem key={f.value} value={f.value} className={"flex-1"}>
|
||||
{f.label}
|
||||
<span className={"tabular-nums"}>({counts[f.value]})</span>
|
||||
</SwitchItem>
|
||||
))}
|
||||
</SwitchItemGroup>
|
||||
<DropdownMenu key={i18n.language} open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 h-9 px-2 rounded-md",
|
||||
"text-sm text-nb-gray-100",
|
||||
"outline-none hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900 transition-colors duration-150",
|
||||
"disabled:opacity-50 disabled:pointer-events-none",
|
||||
"wails-no-draggable",
|
||||
)}
|
||||
>
|
||||
<ListFilter size={14} className={"shrink-0"} />
|
||||
<span>
|
||||
{active.label}{" "}
|
||||
<span className={"tabular-nums"}>
|
||||
({counts[active.value]})
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={"text-nb-gray-400 ml-0.5 shrink-0"}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={"end"} className={"min-w-[10rem]"}>
|
||||
{filters.map((f) => {
|
||||
const checked = f.value === value;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={f.value}
|
||||
onClick={() => handleSelect(f.value)}
|
||||
className={"gap-2"}
|
||||
>
|
||||
<span className={"flex-1 truncate"}>
|
||||
{f.label}{" "}
|
||||
<span className={"tabular-nums"}>
|
||||
({counts[f.value]})
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"w-4 shrink-0 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
{checked && (
|
||||
<CheckIcon
|
||||
size={14}
|
||||
className={"text-netbird"}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,17 +7,11 @@ import { SearchInput } from "@/components/SearchInput";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { NoResults } from "@/components/NoResults";
|
||||
import { useStatus } from "@/modules/daemon-status/StatusContext";
|
||||
import {
|
||||
formatShortcut,
|
||||
useKeyboardShortcut,
|
||||
} from "@/lib/useKeyboardShortcut";
|
||||
import { PeerFilters, StatusFilter } from "./PeerFilters";
|
||||
import { PeersList } from "./PeersList";
|
||||
|
||||
const isOnline = (connStatus: string) => connStatus === "Connected";
|
||||
|
||||
const SEARCH_SHORTCUT = { key: "k", cmd: true } as const;
|
||||
|
||||
export const Peers = () => {
|
||||
const { t } = useTranslation();
|
||||
const { status } = useStatus();
|
||||
@@ -32,11 +26,6 @@ export const Peers = () => {
|
||||
searchRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useKeyboardShortcut(SEARCH_SHORTCUT, () => {
|
||||
searchRef.current?.focus();
|
||||
searchRef.current?.select();
|
||||
});
|
||||
|
||||
const isConnected = status?.status === "Connected";
|
||||
const peers = status?.peers ?? [];
|
||||
|
||||
@@ -88,31 +77,21 @@ export const Peers = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4"}>
|
||||
<div className={"flex flex-col gap-3 px-6"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("peers.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
shortcut={formatShortcut(SEARCH_SHORTCUT)}
|
||||
/>
|
||||
<PeerFilters
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
counts={counts}
|
||||
/>
|
||||
<div className={"flex flex-col w-full h-full min-h-0"}>
|
||||
<div className={"flex items-center gap-2 px-6 py-2.5 border-b border-nb-gray-910"}>
|
||||
<div className={"flex-1 min-w-0"}>
|
||||
<SearchInput
|
||||
ref={searchRef}
|
||||
placeholder={t("peers.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<PeerFilters value={statusFilter} onChange={setStatusFilter} counts={counts} />
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"auto"}
|
||||
className={"flex-1 min-h-0 overflow-hidden mt-3"}
|
||||
>
|
||||
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}>
|
||||
<ScrollArea.Viewport className={"h-full w-full"}>
|
||||
{filtered.length === 0 ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<PeersList data={filtered} />
|
||||
)}
|
||||
{filtered.length === 0 ? <NoResults /> : <PeersList data={filtered} />}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PeersList = ({ data }: { data: PeerStatus[] }) => {
|
||||
key={peer.pubKey}
|
||||
onClick={() => setSelected(peer)}
|
||||
className={cn(
|
||||
"group flex items-start gap-2.5 px-7 py-3 min-w-0",
|
||||
"group flex items-start gap-2.5 px-7 py-3 min-w-0 first:mt-2",
|
||||
"hover:bg-nb-gray-900/40 transition-colors",
|
||||
"wails-no-draggable cursor-pointer",
|
||||
)}
|
||||
|
||||
@@ -310,7 +310,7 @@
|
||||
"sessionAboutToExpire.extendFailedTitle": "Sitzungsverlängerung fehlgeschlagen",
|
||||
"sessionAboutToExpire.logoutFailedTitle": "Abmeldung fehlgeschlagen",
|
||||
|
||||
"peers.search.placeholder": "Nach Peer-Name, DNS oder IP-Adresse suchen",
|
||||
"peers.search.placeholder": "Nach Name oder IP suchen",
|
||||
"peers.filter.all": "Alle",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
@@ -335,7 +335,7 @@
|
||||
"peers.details.p2p": "P2P",
|
||||
"peers.details.rosenpass": "Rosenpass aktiviert",
|
||||
|
||||
"networks.search.placeholder": "Nach Netzwerk, Bereich oder Domain suchen",
|
||||
"networks.search.placeholder": "Nach Netzwerk oder Domain suchen",
|
||||
"networks.filter.all": "Alle",
|
||||
"networks.filter.active": "Aktiv",
|
||||
"networks.filter.overlapping": "Überlappend",
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
"sessionAboutToExpire.expired": "Session expired",
|
||||
"sessionAboutToExpire.extendFailedTitle": "Extend Session Failed",
|
||||
|
||||
"peers.search.placeholder": "Search by peer name, DNS or IP address",
|
||||
"peers.search.placeholder": "Search by name or IP",
|
||||
"peers.filter.all": "All",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
@@ -351,7 +351,7 @@
|
||||
"peers.details.p2p": "P2P",
|
||||
"peers.details.rosenpass": "Rosenpass enabled",
|
||||
|
||||
"networks.search.placeholder": "Search by network, range or domain",
|
||||
"networks.search.placeholder": "Search by network or domain",
|
||||
"networks.filter.all": "All",
|
||||
"networks.filter.active": "Active",
|
||||
"networks.filter.overlapping": "Overlapping",
|
||||
|
||||
@@ -310,7 +310,7 @@
|
||||
"sessionAboutToExpire.extendFailedTitle": "A munkamenet meghosszabbítása sikertelen",
|
||||
"sessionAboutToExpire.logoutFailedTitle": "Kijelentkezés sikertelen",
|
||||
|
||||
"peers.search.placeholder": "Keresés társ neve, DNS vagy IP-cím alapján",
|
||||
"peers.search.placeholder": "Keresés név vagy IP alapján",
|
||||
"peers.filter.all": "Összes",
|
||||
"peers.filter.online": "Online",
|
||||
"peers.filter.offline": "Offline",
|
||||
@@ -335,7 +335,7 @@
|
||||
"peers.details.p2p": "P2P",
|
||||
"peers.details.rosenpass": "Rosenpass engedélyezve",
|
||||
|
||||
"networks.search.placeholder": "Keresés hálózat, tartomány vagy domain alapján",
|
||||
"networks.search.placeholder": "Keresés hálózat vagy domain alapján",
|
||||
"networks.filter.all": "Összes",
|
||||
"networks.filter.active": "Aktív",
|
||||
"networks.filter.overlapping": "Átfedő",
|
||||
|
||||
Reference in New Issue
Block a user