|
1 | | -import { GET_IP_NAMESPACES } from "@/entities/ipam/api/ip-namespaces"; |
2 | | -import { IpamNamespace } from "@/shared/api/graphql/generated/graphql"; |
3 | | -import useQuery from "@/shared/api/graphql/useQuery"; |
| 1 | +import { useGetIpNamespaceList } from "@/entities/ipam/namespaces/domain/get-ip-namespace-list.query"; |
| 2 | +import { useCurrentIpNamespace } from "@/entities/ipam/namespaces/ui/ip-namespace-provider"; |
| 3 | +import { getNodeLabel } from "@/entities/nodes/object/utils/get-node-label"; |
4 | 4 | import { constructPath } from "@/shared/api/rest/fetch"; |
| 5 | +import { Popover, PopoverTrigger } from "@/shared/components/aria/popover"; |
5 | 6 | import { LinkButton } from "@/shared/components/buttons/button-primitive"; |
6 | | -import { Col } from "@/shared/components/container"; |
7 | | -import { Skeleton } from "@/shared/components/skeleton"; |
8 | | -import { |
9 | | - Combobox, |
10 | | - ComboboxContent, |
11 | | - ComboboxItem, |
12 | | - ComboboxList, |
13 | | - ComboboxTrigger, |
14 | | -} from "@/shared/components/ui/combobox"; |
15 | | -import { Icon } from "@iconify-icon/react"; |
16 | | -import { useSetAtom } from "jotai"; |
17 | | -import { useEffect, useId } from "react"; |
18 | | -import { useNavigate, useParams } from "react-router"; |
19 | | -import { StringParam, useQueryParam } from "use-query-params"; |
20 | | -import { defaultIpNamespaceAtom } from "./common/namespace.state"; |
21 | | -import { constructPathForIpam } from "./common/utils"; |
22 | | -import { IPAM_QSP, IPAM_ROUTE, IPAM_TABS, NAMESPACE_GENERIC } from "./constants"; |
| 7 | +import { Col, Row } from "@/shared/components/container"; |
| 8 | +import ErrorScreen from "@/shared/components/errors/error-screen"; |
| 9 | +import { ComboboxEmpty, ComboboxItem, ComboboxList } from "@/shared/components/ui/combobox"; |
| 10 | +import { Spinner } from "@/shared/components/ui/spinner"; |
| 11 | +import { focusVisibleStyle } from "@/shared/components/ui/style"; |
| 12 | +import { classNames, debounce } from "@/shared/utils/common"; |
| 13 | +import { ChevronsUpDownIcon } from "lucide-react"; |
| 14 | +import React from "react"; |
| 15 | +import { Button as AriaButton } from "react-aria-components"; |
23 | 16 |
|
24 | | -export default function IpNamespaceSelector() { |
25 | | - const { loading, data, error } = useQuery(GET_IP_NAMESPACES); |
| 17 | +interface IpNamespaceSelectorProps { |
| 18 | + className?: string; |
| 19 | +} |
26 | 20 |
|
27 | | - if (loading) { |
28 | | - return <Skeleton className="h-10 w-80" />; |
29 | | - } |
| 21 | +export default function IpNamespaceSelector({ className }: IpNamespaceSelectorProps) { |
| 22 | + const { currentIpNamespace } = useCurrentIpNamespace(); |
30 | 23 |
|
31 | | - if (error) { |
32 | | - return null; |
33 | | - } |
| 24 | + return ( |
| 25 | + <div className={classNames("flex gap-2 items-center", className)}> |
| 26 | + <PopoverTrigger> |
| 27 | + <AriaButton |
| 28 | + data-testid="namespace-select" |
| 29 | + className={classNames( |
| 30 | + focusVisibleStyle, |
| 31 | + "flex flex-col w-full rounded-md p-1 m-1", |
| 32 | + "border border-transparent", |
| 33 | + "hover:bg-gray-100" |
| 34 | + )} |
| 35 | + > |
| 36 | + <Row className="text-xs text-gray-600">IP Namespace</Row> |
| 37 | + <Row className="text-sm"> |
| 38 | + {getNodeLabel(currentIpNamespace)} |
| 39 | + <ChevronsUpDownIcon className="ml-auto text-gray-600 size-3.5" /> |
| 40 | + </Row> |
| 41 | + </AriaButton> |
34 | 42 |
|
35 | | - const namespaces = data?.[NAMESPACE_GENERIC]?.edges.map((edge: any) => edge.node) ?? []; |
| 43 | + <Popover placement="bottom start"> |
| 44 | + <IpNamespaceComboboxList /> |
36 | 45 |
|
37 | | - return <IpNamespaceSelectorContent namespaces={namespaces} />; |
| 46 | + <Col className="border-t border-neutral-200"> |
| 47 | + <LinkButton |
| 48 | + to={constructPath("/ipam/namespaces")} |
| 49 | + variant="ghost" |
| 50 | + size="sm" |
| 51 | + className="text-xs justify-start m-2" |
| 52 | + > |
| 53 | + View all IP namespaces |
| 54 | + </LinkButton> |
| 55 | + </Col> |
| 56 | + </Popover> |
| 57 | + </PopoverTrigger> |
| 58 | + </div> |
| 59 | + ); |
38 | 60 | } |
39 | 61 |
|
40 | | -type IpNamespaceSelectorContentProps = { |
41 | | - namespaces: Array<IpamNamespace>; |
42 | | -}; |
43 | | - |
44 | | -const IpNamespaceSelectorContent = ({ namespaces }: IpNamespaceSelectorContentProps) => { |
45 | | - const { prefix, ip_address } = useParams(); |
46 | | - const navigate = useNavigate(); |
47 | | - const [ipamTab] = useQueryParam(IPAM_QSP.TAB, StringParam); |
48 | | - const [namespaceQSP, setNamespaceQSP] = useQueryParam(IPAM_QSP.NAMESPACE, StringParam); |
49 | | - const setDefaultIpNamespace = useSetAtom(defaultIpNamespaceAtom); |
50 | | - const selectedNamespace = namespaces.find((result) => result.id === namespaceQSP); |
51 | | - const defaultNamespace = namespaces.find((result) => result.default?.value === true); |
52 | | - const currentNamespace = selectedNamespace || defaultNamespace; |
53 | | - const id = useId(); |
| 62 | +function IpNamespaceComboboxList({ ...props }) { |
| 63 | + const [search, setSearch] = React.useState(""); |
| 64 | + const { currentIpNamespace, setCurrentIpNamespace } = useCurrentIpNamespace(); |
| 65 | + const { isPending, data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = |
| 66 | + useGetIpNamespaceList({ |
| 67 | + filters: search ? [{ name: "any__value", value: search }] : undefined, |
| 68 | + }); |
54 | 69 |
|
55 | | - useEffect(() => { |
56 | | - if (defaultNamespace) { |
57 | | - setDefaultIpNamespace(defaultNamespace.id); |
58 | | - } |
59 | | - }, []); |
60 | | - |
61 | | - const handleNamespaceChange = (newValue: IpamNamespace) => { |
62 | | - if (!newValue.id || newValue.id === defaultNamespace?.id) { |
63 | | - setNamespaceQSP(undefined); // Removes QSP for default namespace |
64 | | - } else { |
65 | | - setNamespaceQSP(newValue.id); |
66 | | - } |
| 70 | + if (error) { |
| 71 | + return <ErrorScreen message={error.message} />; |
| 72 | + } |
67 | 73 |
|
68 | | - if (prefix || ip_address) { |
69 | | - // Redirects to main lists on namespace switch |
70 | | - if (ipamTab === IPAM_TABS.IP_DETAILS) { |
71 | | - // Redirects to main IP Addresses view |
72 | | - navigate(constructPathForIpam(IPAM_ROUTE.ADDRESSES)); |
73 | | - } else { |
74 | | - // Redirects to main Prefixes view |
75 | | - navigate(constructPathForIpam(IPAM_ROUTE.PREFIXES)); |
76 | | - } |
77 | | - } |
78 | | - }; |
| 74 | + const setSearchDebounced = debounce(setSearch, 300); |
79 | 75 |
|
80 | 76 | return ( |
81 | | - <div className="flex gap-2 items-center"> |
82 | | - <Icon icon="mdi:chevron-right" /> |
83 | | - <label htmlFor={id}>Namespace</label> |
| 77 | + <ComboboxList |
| 78 | + onValueChange={(newValue) => setSearchDebounced(newValue)} |
| 79 | + shouldFilter={false} |
| 80 | + {...props} |
| 81 | + > |
| 82 | + {isPending ? ( |
| 83 | + <Spinner className="flex justify-center m-2" /> |
| 84 | + ) : ( |
| 85 | + <> |
| 86 | + <ComboboxEmpty>No IP namespace found</ComboboxEmpty> |
84 | 87 |
|
85 | | - <Combobox> |
86 | | - <ComboboxTrigger id={id} data-testid="namespace-select"> |
87 | | - {selectedNamespace?.display_label ?? defaultNamespace?.display_label} |
88 | | - </ComboboxTrigger> |
89 | | - |
90 | | - <ComboboxContent align="start" fitTriggerWidth={false}> |
91 | | - <ComboboxList className="max-w-md"> |
92 | | - {namespaces.map((namespace) => ( |
| 88 | + {data.pages.map((page) => { |
| 89 | + return page.map((namespace) => ( |
93 | 90 | <ComboboxItem |
94 | 91 | key={namespace.id} |
95 | 92 | value={namespace.id} |
96 | | - selectedValue={currentNamespace?.id} |
97 | | - onSelect={() => handleNamespaceChange(namespace)} |
| 93 | + selectedValue={currentIpNamespace.id} |
| 94 | + onSelect={() => setCurrentIpNamespace(namespace)} |
98 | 95 | > |
99 | 96 | <div className="overflow-hidden"> |
100 | | - <div className="truncate font-semibold">{namespace.display_label}</div> |
| 97 | + <div className="truncate font-semibold">{getNodeLabel(namespace)}</div> |
101 | 98 | <p className="text-xs truncate text-gray-500">{namespace.description?.value}</p> |
102 | 99 | </div> |
103 | 100 | </ComboboxItem> |
104 | | - ))} |
105 | | - </ComboboxList> |
106 | | - <Col className="border-t border-neutral-200"> |
107 | | - <LinkButton |
108 | | - to={constructPath("/ipam/namespaces")} |
109 | | - variant="ghost" |
110 | | - size="sm" |
111 | | - className="text-xs justify-start m-2" |
112 | | - > |
113 | | - View all IP namespaces |
114 | | - </LinkButton> |
115 | | - </Col> |
116 | | - </ComboboxContent> |
117 | | - </Combobox> |
118 | | - </div> |
| 101 | + )); |
| 102 | + })} |
| 103 | + </> |
| 104 | + )} |
| 105 | + |
| 106 | + {hasNextPage && ( |
| 107 | + <ComboboxItem |
| 108 | + value="Load more" |
| 109 | + onSelect={() => fetchNextPage()} |
| 110 | + disabled={!hasNextPage || isFetchingNextPage} |
| 111 | + className="justify-center text-custom-blue-700" |
| 112 | + > |
| 113 | + {isFetchingNextPage ? "Loading more..." : "Load more"} |
| 114 | + </ComboboxItem> |
| 115 | + )} |
| 116 | + </ComboboxList> |
119 | 117 | ); |
120 | | -}; |
| 118 | +} |
0 commit comments