-
- Showing{" "}
-
- {showingFrom} to {showingTo}
- {" "}
- of {allRows} {text}
-
-
-
-
- table.setPageIndex(0)}
- disabled={!table.getCanPreviousPage()}
- >
-
-
- table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
-
-
-
-
- {currentPage} of {pageCount}
-
-
- table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
-
-
- table.setPageIndex(table.getPageCount() - 1)}
- disabled={!table.getCanNextPage()}
- >
-
-
-
+ const showingFrom = totalRows === 0 ? 0 : (currentPage - 1) * rowsPerPage + 1;
+ const showingTo = Math.min(currentPage * rowsPerPage, totalRows);
+
+ return (
+ pageCount > 1 && (
+
+
+ Showing{" "}
+
+ {showingFrom} to {showingTo}
+ {" "}
+ of {totalRows} {" "}
+ {text}
+ {pageCount > 1 && (
+
+
+
+ table.setPageIndex(0)}
+ disabled={!table.getCanPreviousPage()}
+ >
+
+
+ table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+
+
+
+
+ {currentPage} of {pageCount}
+
+
+ table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+
+
+ table.setPageIndex(table.getPageCount() - 1)}
+ disabled={!table.getCanNextPage()}
+ >
+
+
+
+
+
+ )}
-
- ) : null;
+ )
+ );
}
diff --git a/src/components/table/DataTableResetFilterButton.tsx b/src/components/table/DataTableResetFilterButton.tsx
index 195299bb..22b89e3c 100644
--- a/src/components/table/DataTableResetFilterButton.tsx
+++ b/src/components/table/DataTableResetFilterButton.tsx
@@ -8,48 +8,54 @@ import { useState } from "react";
interface Props
{
table: Table;
onClick: () => void;
+ hasServerSideFilters?: boolean;
}
export default function DataTableResetFilterButton({
table,
onClick,
+ hasServerSideFilters = undefined,
}: Props) {
const [hovered, setHovered] = useState(false);
- const isDisabled =
- table.getState().columnFilters.length <= 0 &&
- table.getState().globalFilter === "";
- return !isDisabled ? (
-
- setHovered(true)}
- onMouseLeave={() => setHovered(false)}
- onClick={(e) => {
- e.preventDefault();
- }}
- >
- 0;
+
+ const showButton = hasServerSideFilters ?? hasClientSideFilters;
+
+ return (
+ showButton && (
+
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ onClick={(e) => {
+ e.preventDefault();
+ }}
>
-
-
-
+
+
+
+
- {
- if (hovered) event.preventDefault();
- }}
- >
-
- Reset Filters & Search
-
-
-
- ) : null;
+ {
+ if (hovered) event.preventDefault();
+ }}
+ >
+
+ Reset Filters & Search
+
+
+
+ )
+ );
}
diff --git a/src/components/ui/GradientFadedBackground.tsx b/src/components/ui/GradientFadedBackground.tsx
index 283615e0..deb02b5e 100644
--- a/src/components/ui/GradientFadedBackground.tsx
+++ b/src/components/ui/GradientFadedBackground.tsx
@@ -1,11 +1,17 @@
+import { cn } from "@utils/helpers";
import * as React from "react";
-export const GradientFadedBackground = () => {
+type Props = {
+ className?: string;
+};
+
+export const GradientFadedBackground = ({ className }: Props) => {
return (
void;
error?: string;
disabled?: boolean;
+ showRemoveButton?: boolean;
preventLeadingAndTrailingDots?: boolean;
allowWildcard?: boolean;
};
@@ -44,6 +45,7 @@ export default function InputDomain({
disabled,
preventLeadingAndTrailingDots,
allowWildcard = true,
+ showRemoveButton = true,
}: Readonly
) {
const [name, setName] = useState(value?.name || "");
@@ -88,14 +90,16 @@ export default function InputDomain({
/>
-
-
-
+ {showRemoveButton && (
+
+
+
+ )}
);
}
diff --git a/src/components/ui/MultipleGroups.tsx b/src/components/ui/MultipleGroups.tsx
index 97dfe462..5e340ca0 100644
--- a/src/components/ui/MultipleGroups.tsx
+++ b/src/components/ui/MultipleGroups.tsx
@@ -9,9 +9,9 @@ import {
import GroupBadge from "@components/ui/GroupBadge";
import PeerBadge from "@components/ui/PeerBadge";
import { cn } from "@utils/helpers";
-import { orderBy } from "lodash";
-import { ArrowRightIcon } from "lucide-react";
+import { ArrowRightIcon, PencilLineIcon } from "lucide-react";
import * as React from "react";
+import { usePermissions } from "@/contexts/PermissionsProvider";
import { Group } from "@/interfaces/Group";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
@@ -30,8 +30,17 @@ export default function MultipleGroups({
onClick,
className,
}: Readonly) {
- if (!groups) return ;
- const orderedGroups = orderBy(groups, ["peers_count", "name"], ["desc"]);
+ const { permission } = usePermissions();
+
+ if (!groups || groups?.length === 0) return ;
+ const orderedGroups = groups.sort((a, b) => {
+ if (a.name === "All") return 1;
+ if (b.name === "All") return -1;
+ const aPeerCount = a.peers_count ?? 0;
+ const bPeerCount = b.peers_count ?? 0;
+ if (aPeerCount !== bPeerCount) return bPeerCount - aPeerCount;
+ return a.name.localeCompare(b.name);
+ });
const firstGroup = orderedGroups.length > 0 ? orderedGroups[0] : undefined;
const otherGroups = orderedGroups.length > 0 ? orderedGroups.slice(1) : [];
@@ -48,12 +57,22 @@ export default function MultipleGroups({
data-cy={"multiple-groups"}
onClick={onClick}
>
- {firstGroup && }
+ {firstGroup && (
+
+ )}
{otherGroups && otherGroups.length > 0 && (
+ {otherGroups.length}
@@ -98,3 +117,15 @@ export default function MultipleGroups({
);
}
+
+export const TransparentEditIconButton = () => {
+ return (
+
+ );
+};
diff --git a/src/components/ui/NoResults.tsx b/src/components/ui/NoResults.tsx
index ab6d1349..39466506 100644
--- a/src/components/ui/NoResults.tsx
+++ b/src/components/ui/NoResults.tsx
@@ -1,7 +1,9 @@
+import Button from "@components/Button";
import Paragraph from "@components/Paragraph";
import { cn } from "@utils/helpers";
import { FilterX } from "lucide-react";
-import React from "react";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import React, { useCallback } from "react";
import Skeleton from "react-loading-skeleton";
type Props = {
@@ -10,6 +12,8 @@ type Props = {
description?: string;
children?: React.ReactNode;
className?: string;
+ hasFiltersApplied?: boolean;
+ onResetFilters?: () => void;
};
export default function NoResults({
icon,
@@ -17,7 +21,32 @@ export default function NoResults({
description = "We couldn't find any results. Please try a different search term or change your filters.",
children,
className,
+ hasFiltersApplied = false,
+ onResetFilters,
}: Props) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ const handleResetClick = useCallback(() => {
+ if (onResetFilters) {
+ onResetFilters();
+
+ const params = new URLSearchParams();
+
+ const page_size = searchParams.get("page_size");
+
+ params.set("page", "1");
+
+ if (page_size) {
+ params.set("page_size", page_size);
+ }
+
+ const newUrl = `${pathname}?${params.toString()}`;
+ router.push(newUrl);
+ }
+ }, [onResetFilters, router, pathname, searchParams]);
+
return (
{description}
+ {hasFiltersApplied && onResetFilters && (
+
+
+ Reset Filters & Search
+
+ )}
{children}
diff --git a/src/components/ui/PolicyDirection.tsx b/src/components/ui/PolicyDirection.tsx
index f19c1722..145184a2 100644
--- a/src/components/ui/PolicyDirection.tsx
+++ b/src/components/ui/PolicyDirection.tsx
@@ -1,13 +1,15 @@
import Badge from "@components/Badge";
import { cn } from "@utils/helpers";
-import React, { useEffect } from "react";
+import React, { useEffect, useMemo } from "react";
import LongArrowLeftIcon from "@/assets/icons/LongArrowLeftIcon";
+import { PolicyRuleResource } from "@/interfaces/Policy";
type Props = {
disabled?: boolean;
value: Direction;
onChange: (value: Direction) => void;
className?: string;
+ destinationResource?: PolicyRuleResource;
};
export type Direction = "bi" | "in" | "out";
@@ -17,31 +19,8 @@ export default function PolicyDirection({
value,
onChange,
className,
-}: Props) {
- const toggleIn = () => {
- if (value == "in") {
- onChange("out");
- return;
- }
- if (value == "bi") {
- onChange("out");
- } else {
- onChange("bi");
- }
- };
-
- const toggleOut = () => {
- if (value == "out") {
- onChange("in");
- return;
- }
- if (value == "bi") {
- onChange("in");
- } else {
- onChange("bi");
- }
- };
-
+ destinationResource,
+}: Readonly) {
const toggleDirection = () => {
if (value == "bi") {
onChange("in");
@@ -55,8 +34,34 @@ export default function PolicyDirection({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabled]);
+ const topBadgeClass = useMemo(() => {
+ if (destinationResource) return "blueDark";
+ if (value === "bi") return "green";
+ if (value === "in") return "blueDark";
+ return "gray";
+ }, [value, destinationResource]);
+
+ const topArrowClass = useMemo(() => {
+ if (destinationResource) return "fill-sky-500";
+ if (value === "bi") return "fill-green-500";
+ if (value === "in") return "fill-sky-500";
+ return "fill-gray-500";
+ }, [value, destinationResource]);
+
+ const bottomBadgeClass = useMemo(() => {
+ if (destinationResource) return "gray";
+ if (value === "bi") return "green";
+ return "gray";
+ }, [value, destinationResource]);
+
+ const bottomArrowClass = useMemo(() => {
+ if (destinationResource) return "fill-gray-500";
+ if (value === "bi") return "fill-green-500";
+ return "fill-gray-500";
+ }, [value, destinationResource]);
+
return (
-
-
+
-
+
-
+
);
}
diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts
index 1676f242..1647dae1 100644
--- a/src/hooks/useSearch.ts
+++ b/src/hooks/useSearch.ts
@@ -32,7 +32,9 @@ export function useSearch(
string,
(event: ChangeEvent | string) => void,
(querty: string) => void,
+ boolean,
] {
+ const [isSearching, setIsSearching] = useState(false);
const isMounted = useRef(false);
const [query, setQuery] = useState(initialQuery);
const prevCollection = usePrevious(collection);
@@ -62,6 +64,7 @@ export function useSearch(
setFilteredCollection(
filterCollection(collection, predicate, query, filter),
);
+ setIsSearching(false);
}
},
debounce,
@@ -75,8 +78,10 @@ export function useSearch(
!isEqual(predicate, prevPredicate) ||
!isEqual(query, prevQuery) ||
!isEqual(filter, prevFilter)
- )
+ ) {
+ if (!isEqual(query, prevQuery)) setIsSearching(true);
debouncedFilterCollection(collection, predicate, query, filter);
+ }
}, [collection, predicate, query, filter]);
useEffect(() => {
@@ -87,5 +92,5 @@ export function useSearch(
};
}, []);
- return [filteredCollection, query, handleChange, setQuery];
+ return [filteredCollection, query, handleChange, setQuery, isSearching];
}
diff --git a/src/interfaces/Nameserver.ts b/src/interfaces/Nameserver.ts
index cee49f3c..4b4c09c9 100644
--- a/src/interfaces/Nameserver.ts
+++ b/src/interfaces/Nameserver.ts
@@ -104,4 +104,50 @@ export const NameserverPresets: Record = {
enabled: true,
search_domains_enabled: false,
},
+ DNS0: {
+ name: "DNS0.EU",
+ description: "DNS0.EU DNS Servers",
+ primary: true,
+ domains: [],
+ nameservers: [
+ {
+ ip: "193.110.81.0",
+ ns_type: "udp",
+ port: 53,
+ id: "1",
+ },
+ {
+ ip: "185.253.5.0",
+ ns_type: "udp",
+ port: 53,
+ id: "2",
+ },
+ ],
+ groups: [],
+ enabled: true,
+ search_domains_enabled: false,
+ },
+ DNS0Zero: {
+ name: "DNS0.EU Zero",
+ description: "DNS0.EU Zero DNS Servers",
+ primary: true,
+ domains: [],
+ nameservers: [
+ {
+ ip: "193.110.81.9",
+ ns_type: "udp",
+ port: 53,
+ id: "1",
+ },
+ {
+ ip: "185.253.5.9",
+ ns_type: "udp",
+ port: 53,
+ id: "2",
+ },
+ ],
+ groups: [],
+ enabled: true,
+ search_domains_enabled: false,
+ },
};
diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx
index d28e5cde..9861b0d1 100644
--- a/src/layouts/Header.tsx
+++ b/src/layouts/Header.tsx
@@ -1,15 +1,13 @@
"use client";
import Button from "@components/Button";
+import { NetBirdLogo } from "@components/NetBirdLogo";
import { AnnouncementBanner } from "@components/ui/AnnouncementBanner";
import UserDropdown from "@components/ui/UserDropdown";
import { cn } from "@utils/helpers";
import { MenuIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-react";
-import Image from "next/image";
import { useRouter } from "next/navigation";
-import React, { useMemo } from "react";
-import NetBirdLogo from "@/assets/netbird.svg";
-import NetBirdLogoFull from "@/assets/netbird-full.svg";
+import React from "react";
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
@@ -18,25 +16,6 @@ export const headerHeight = 75;
export default function NavbarWithDropdown() {
const router = useRouter();
- const Logo = useMemo(() => {
- return (
- <>
-
-
- >
- );
- }, []);
-
const { toggleMobileNav } = useApplicationContext();
const { bannerHeight } = useAnnouncement();
const { isRestricted } = usePermissions();
@@ -78,7 +57,7 @@ export default function NavbarWithDropdown() {
"cursor-pointer hover:opacity-70 transition-all mr-auto"
}
>
- {Logo}
+
diff --git a/src/modules/access-control/AccessControlModal.tsx b/src/modules/access-control/AccessControlModal.tsx
index 62fe001e..dbe226d3 100644
--- a/src/modules/access-control/AccessControlModal.tsx
+++ b/src/modules/access-control/AccessControlModal.tsx
@@ -1,6 +1,7 @@
"use client";
import Button from "@components/Button";
+import { Callout } from "@components/Callout";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
@@ -29,6 +30,7 @@ import { Textarea } from "@components/Textarea";
import PolicyDirection from "@components/ui/PolicyDirection";
import { cn } from "@utils/helpers";
import {
+ AlertCircleIcon,
ArrowRightLeft,
ExternalLinkIcon,
FolderDown,
@@ -130,11 +132,13 @@ export function AccessControlModalContent({
const { permission } = usePermissions();
const {
- portAndDirectionDisabled,
+ portDisabled,
destinationGroups,
direction,
ports,
sourceGroups,
+ destinationHasResources,
+ destinationOnlyResources,
setSourceGroups,
setDestinationGroups,
setPorts,
@@ -156,6 +160,7 @@ export function AccessControlModalContent({
setDestinationResource,
portRanges,
setPortRanges,
+ hasPortSupport,
} = useAccessControl({
policy,
postureCheckTemplates,
@@ -183,17 +188,10 @@ export function AccessControlModalContent({
const handleProtocolChange = (p: Protocol) => {
setProtocol(p);
- if (p == "icmp") {
+ if (!hasPortSupport(p)) {
setPorts([]);
setPortRanges([]);
}
- if (p == "all") {
- setPorts([]);
- setPortRanges([]);
- }
- if (p == "tcp" || p == "udp") {
- setDirection("in");
- }
};
const close = () => {
@@ -301,7 +299,8 @@ export function AccessControlModalContent({
@@ -329,10 +328,28 @@ export function AccessControlModalContent({
+ {destinationHasResources &&
+ !destinationOnlyResources &&
+ direction === "bi" && (
+
@@ -352,7 +369,7 @@ export function AccessControlModalContent({
onPortsChange={setPorts}
portRanges={portRanges}
onPortRangesChange={setPortRanges}
- disabled={portAndDirectionDisabled}
+ disabled={portDisabled}
/>
diff --git a/src/modules/access-control/table/AccessControlDirectionCell.tsx b/src/modules/access-control/table/AccessControlDirectionCell.tsx
index 541bda27..90759500 100644
--- a/src/modules/access-control/table/AccessControlDirectionCell.tsx
+++ b/src/modules/access-control/table/AccessControlDirectionCell.tsx
@@ -7,17 +7,20 @@ import { Policy } from "@/interfaces/Policy";
type Props = {
policy: Policy;
};
-export default function AccessControlDirectionCell({ policy }: Props) {
+export default function AccessControlDirectionCell({
+ policy,
+}: Readonly