mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
* Update axa oidc library and package.json * Update ACL port state to show correct value * Filter user groups by unique groups only * Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations * Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
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";
|
|
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
|
import { sortBy, trim, unionBy } from "lodash";
|
|
import { ChevronsUpDown, SearchIcon } from "lucide-react";
|
|
import * as React from "react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
|
import { useElementSize } from "@/hooks/useElementSize";
|
|
import { GroupedRoute, Route } from "@/interfaces/Route";
|
|
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
|
|
|
interface MultiSelectProps {
|
|
value?: GroupedRoute;
|
|
onChange: React.Dispatch<React.SetStateAction<GroupedRoute | undefined>>;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function NetworkRouteSelector({
|
|
onChange,
|
|
value,
|
|
disabled = false,
|
|
}: MultiSelectProps) {
|
|
const { data: routes } = useFetchApi<Route[]>("/routes");
|
|
const groupedRoutes = useGroupedRoutes({ routes });
|
|
|
|
const [dropdownOptions, setDropdownOptions] = useState<GroupedRoute[]>([]);
|
|
const searchRef = React.useRef<HTMLInputElement>(null);
|
|
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
|
const [search, setSearch] = useState("");
|
|
|
|
// Update dropdown options when peers change
|
|
useEffect(() => {
|
|
if (!groupedRoutes) return;
|
|
|
|
// Sort
|
|
let options = sortBy([...groupedRoutes], "network_id") as GroupedRoute[];
|
|
|
|
// Filter out networks that are groups
|
|
options = options.filter((peer) => {
|
|
return !peer.is_using_route_groups;
|
|
});
|
|
|
|
setDropdownOptions(unionBy(options, dropdownOptions, "name"));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [groupedRoutes]);
|
|
|
|
const togglePeer = (network: GroupedRoute) => {
|
|
const isSelected = value && value.id == network.id;
|
|
if (isSelected) {
|
|
onChange(undefined);
|
|
} else {
|
|
onChange(network);
|
|
setSearch("");
|
|
}
|
|
};
|
|
|
|
const notFound = useMemo(() => {
|
|
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) ||
|
|
domains.includes(search)
|
|
);
|
|
}).length > 0;
|
|
return isSearching && !found;
|
|
}, [search, dropdownOptions]);
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<Popover
|
|
open={open}
|
|
onOpenChange={(isOpen) => {
|
|
if (!isOpen) {
|
|
setTimeout(() => {
|
|
setSearch("");
|
|
}, 100);
|
|
}
|
|
setOpen(isOpen);
|
|
}}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
className={cn(
|
|
"min-h-[42px] w-full relative items-center group",
|
|
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
|
|
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer enabled:hover:dark:bg-nb-gray-900/50",
|
|
"disabled:opacity-40 disabled:cursor-default",
|
|
)}
|
|
disabled={disabled}
|
|
ref={inputRef}
|
|
>
|
|
<div
|
|
className={
|
|
"flex items-center w-full gap-2 border-nb-gray-700 flex-wrap h-full"
|
|
}
|
|
>
|
|
{value ? (
|
|
<div
|
|
className={
|
|
"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} />
|
|
<TextWithTooltip text={value.network_id} maxChars={15} />
|
|
</div>
|
|
|
|
<div
|
|
className={
|
|
"text-neutral-500 mt-.5 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
|
}
|
|
>
|
|
{value.network}
|
|
</div>
|
|
<DomainList domains={value?.domains} />
|
|
</div>
|
|
) : (
|
|
<span>Select an existing network...</span>
|
|
)}
|
|
</div>
|
|
|
|
<ChevronsUpDown size={18} className={"shrink-0"} />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-full p-0 shadow-sm shadow-nb-gray-950"
|
|
style={{
|
|
width: width,
|
|
}}
|
|
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 network by name or cidr..."}
|
|
/>
|
|
<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={"p-2"}>
|
|
{dropdownOptions.length == 0 && !notFound && (
|
|
<div
|
|
className={
|
|
"text-center pb-2 text-nb-gray-500 max-w-xs mx-auto"
|
|
}
|
|
>
|
|
{"Seems like you don't have any network routes created yet."}
|
|
</div>
|
|
)}
|
|
{notFound && (
|
|
<div className={"text-center pb-2 text-nb-gray-500"}>
|
|
There are no networks matching your search.
|
|
</div>
|
|
)}
|
|
<CommandGroup>
|
|
<div
|
|
className={
|
|
"max-h-[180px] overflow-y-auto flex flex-col gap-1"
|
|
}
|
|
>
|
|
{dropdownOptions.map((option) => {
|
|
return (
|
|
<CommandItem
|
|
key={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} />
|
|
<TextWithTooltip
|
|
text={option.network_id}
|
|
maxChars={15}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className={
|
|
"text-neutral-500 mt-.5 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
|
}
|
|
>
|
|
{option.network}
|
|
</div>
|
|
<DomainList domains={option?.domains} />
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</div>
|
|
</CommandGroup>
|
|
</div>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</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>
|
|
)
|
|
);
|
|
}
|