mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-31 12:01:29 +02:00
add virtual scroll and fix empty results overlay
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"tailwind-merge": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
14
client/ui/frontend/pnpm-lock.yaml
generated
14
client/ui/frontend/pnpm-lock.yaml
generated
@@ -74,6 +74,9 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^7.1.3
|
||||
version: 7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-virtuoso:
|
||||
specifier: ^4.12.5
|
||||
version: 4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
tailwind-merge:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.1
|
||||
@@ -1456,6 +1459,12 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-virtuoso@4.18.7:
|
||||
resolution: {integrity: sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g==}
|
||||
peerDependencies:
|
||||
react: '>=16 || >=17 || >= 18 || >= 19'
|
||||
react-dom: '>=16 || >=17 || >= 18 || >=19'
|
||||
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2800,6 +2809,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.29
|
||||
|
||||
react-virtuoso@4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
@@ -16,7 +16,7 @@ export const NoResults = ({ icon = FunnelXIcon, title, description }: Props) =>
|
||||
icon={icon}
|
||||
title={title ?? t("common.noResults.title")}
|
||||
description={description ?? t("common.noResults.description")}
|
||||
className={"relative -top-[3.8rem]"}
|
||||
className={"relative -top-[3.8rem] pointer-events-none"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentType,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { GlobeIcon, Layers3Icon, type LucideProps, NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
import type { Network } from "@bindings/services/models.js";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -67,6 +74,7 @@ export const Networks = () => {
|
||||
const { networkRoutes, toggleNetwork, setNetworksSelected } = useNetworks();
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState<NetworkFilter>("all");
|
||||
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,28 +170,37 @@ export const Networks = () => {
|
||||
</div>
|
||||
<NetworkFilters value={filter} onChange={setFilter} counts={counts} />
|
||||
</div>
|
||||
<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} />
|
||||
)}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb
|
||||
className={
|
||||
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
|
||||
}
|
||||
/>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
{filtered.length === 0 ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}>
|
||||
<ScrollArea.Viewport
|
||||
ref={setScrollParent}
|
||||
className={"h-full w-full"}
|
||||
>
|
||||
{scrollParent && (
|
||||
<NetworksList
|
||||
data={filtered}
|
||||
onToggle={toggleNetwork}
|
||||
scrollParent={scrollParent}
|
||||
/>
|
||||
)}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb
|
||||
className={
|
||||
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
|
||||
}
|
||||
/>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
)}
|
||||
{filtered.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -218,18 +235,25 @@ export const Networks = () => {
|
||||
type NetworksListProps = {
|
||||
data: Network[];
|
||||
onToggle: (id: string, selected: boolean) => void;
|
||||
scrollParent: HTMLElement;
|
||||
};
|
||||
|
||||
const NetworksList = ({ data, onToggle }: NetworksListProps) => {
|
||||
const NetworksHeader = () => <div className={"h-2"} />;
|
||||
|
||||
const NetworksList = ({ data, onToggle, scrollParent }: NetworksListProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ul className={"flex flex-col"}>
|
||||
{data.map((n) => (
|
||||
<li
|
||||
key={n.id}
|
||||
<Virtuoso
|
||||
data={data}
|
||||
customScrollParent={scrollParent}
|
||||
increaseViewportBy={400}
|
||||
computeItemKey={(_, n) => n.id}
|
||||
components={{ Header: NetworksHeader }}
|
||||
itemContent={(_, n) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex items-start gap-2.5 pl-6 pr-9 py-3 min-w-0 first:mt-2",
|
||||
"group relative flex items-start gap-2.5 pl-6 pr-9 py-3 min-w-0",
|
||||
"hover:bg-nb-gray-900/40 transition-colors",
|
||||
"wails-no-draggable",
|
||||
)}
|
||||
@@ -265,9 +289,9 @@ const NetworksList = ({ data, onToggle }: NetworksListProps) => {
|
||||
label={n.selected ? t("networks.selected") : t("networks.unselected")}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { ChevronRightIcon, MonitorSmartphoneIcon } from "lucide-react";
|
||||
import type { PeerStatus } from "@bindings/services/models.js";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -45,6 +46,7 @@ export const Peers = () => {
|
||||
const { status } = useStatus();
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,41 +132,61 @@ export const Peers = () => {
|
||||
</div>
|
||||
<PeerFilters value={statusFilter} onChange={setStatusFilter} counts={counts} />
|
||||
</div>
|
||||
<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} />}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb
|
||||
className={
|
||||
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
|
||||
}
|
||||
/>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
{filtered.length === 0 ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}>
|
||||
<ScrollArea.Viewport
|
||||
ref={setScrollParent}
|
||||
className={"h-full w-full"}
|
||||
>
|
||||
{scrollParent && (
|
||||
<PeersList data={filtered} scrollParent={scrollParent} />
|
||||
)}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation={"vertical"}
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb
|
||||
className={
|
||||
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
|
||||
}
|
||||
/>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PeersList = ({ data }: { data: PeerStatus[] }) => {
|
||||
const ListTopSpacer = () => <div className={"h-2"} />;
|
||||
|
||||
type PeersListProps = {
|
||||
data: PeerStatus[];
|
||||
scrollParent: HTMLElement;
|
||||
};
|
||||
|
||||
const PeersList = ({ data, scrollParent }: PeersListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setSelected } = usePeerDetail();
|
||||
|
||||
return (
|
||||
<ul className={"flex flex-col"}>
|
||||
{data.map((peer) => {
|
||||
<Virtuoso
|
||||
data={data}
|
||||
customScrollParent={scrollParent}
|
||||
increaseViewportBy={400}
|
||||
computeItemKey={(_, peer) => peer.pubKey}
|
||||
components={{ Header: ListTopSpacer }}
|
||||
itemContent={(_, peer) => {
|
||||
const isConnected = peer.connStatus === "Connected";
|
||||
return (
|
||||
<li
|
||||
key={peer.pubKey}
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex items-start gap-2.5 pl-6 pr-4 py-3 min-w-0 first:mt-2",
|
||||
"group relative flex items-start gap-2.5 pl-6 pr-4 py-3 min-w-0",
|
||||
"hover:bg-nb-gray-900/40 transition-colors",
|
||||
"wails-no-draggable",
|
||||
)}
|
||||
@@ -229,9 +251,9 @@ const PeersList = ({ data }: { data: PeerStatus[] }) => {
|
||||
"opacity-0 group-hover:opacity-100 transition-opacity",
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user