update peers ui

This commit is contained in:
Eduard Gert
2026-05-27 18:01:06 +02:00
parent ad7d7fa881
commit 09f4109b01
10 changed files with 191 additions and 143 deletions
@@ -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>
);
};
+13 -34
View File
@@ -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",
)}