mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2cbe1d2a |
No files matched your search
@@ -1,26 +1,37 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type CalloutVariants = VariantProps<typeof calloutVariants>;
|
||||
|
||||
type Props = {
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
} & CalloutVariants;
|
||||
|
||||
export const calloutVariants = cva(
|
||||
["px-4 py-3.5 rounded-md border text-sm font-normal flex gap-3 font-light"],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-nb-gray-900/60 border-nb-gray-800/80 text-nb-gray-300",
|
||||
warning: "bg-netbird-500/10 border-netbird-400/20 text-netbird-150",
|
||||
info: "bg-sky-400/10 border-sky-400/20 text-sky-100",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const Callout = ({
|
||||
children,
|
||||
icon = <InfoIcon size={14} className={"shrink-0 relative top-[1px]"} />,
|
||||
icon = <InfoIcon size={14} className={"shrink-0 relative top-[2px]"} />,
|
||||
className,
|
||||
variant = "default",
|
||||
}: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"px-4 py-3 rounded-md border text-sm font-normal flex gap-3",
|
||||
"bg-nb-gray-900/60 border-nb-gray-800/80 text-nb-gray-300",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={cn(calloutVariants({ variant }), className)}>
|
||||
{icon}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
+279
-212
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,7 @@ import {
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useEventCallback } from "@/hooks/useEventCallback";
|
||||
@@ -20,8 +21,10 @@ export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T,
|
||||
enabled: boolean = true,
|
||||
overrideValue?: T,
|
||||
): [T, SetValue<T>] {
|
||||
const [tempValue, setTempValue] = useState(initialValue);
|
||||
const [tempValue, setTempValue] = useState(overrideValue ?? initialValue);
|
||||
const isInitialRender = useRef(true);
|
||||
|
||||
// Get from local storage then
|
||||
// parse stored json or return initialValue
|
||||
@@ -31,6 +34,11 @@ export function useLocalStorage<T>(
|
||||
return initialValue;
|
||||
}
|
||||
|
||||
if (isInitialRender.current && overrideValue !== undefined) {
|
||||
isInitialRender.current = false;
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? (parseJSON(item) as T) : initialValue;
|
||||
@@ -95,6 +103,13 @@ export function useLocalStorage<T>(
|
||||
[key, readValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideValue) {
|
||||
setValue(overrideValue);
|
||||
setStoredValue(overrideValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// this only works for other documents, not the current one
|
||||
useEventListener("storage", handleStorageChange);
|
||||
|
||||
|
||||
@@ -22,10 +22,16 @@ export interface PolicyRule {
|
||||
action: string;
|
||||
protocol: Protocol;
|
||||
ports: string[];
|
||||
port_ranges?: PortRange[];
|
||||
sourceResource?: PolicyRuleResource;
|
||||
destinationResource?: PolicyRuleResource;
|
||||
}
|
||||
|
||||
export interface PortRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface PolicyRuleResource {
|
||||
id: string;
|
||||
type: "domain" | "host" | "subnet" | undefined;
|
||||
|
||||
@@ -154,6 +154,8 @@ export function AccessControlModalContent({
|
||||
getPolicyData,
|
||||
destinationResource,
|
||||
setDestinationResource,
|
||||
portRanges,
|
||||
setPortRanges,
|
||||
} = useAccessControl({
|
||||
policy,
|
||||
postureCheckTemplates,
|
||||
@@ -166,15 +168,13 @@ export function AccessControlModalContent({
|
||||
const [tab, setTab] = useState(() => {
|
||||
if (!cell) return "policy";
|
||||
if (cell == "posture_checks") return "posture_checks";
|
||||
if (cell == "name") return "general";
|
||||
return "policy";
|
||||
});
|
||||
|
||||
const continuePostureChecksDisabled = useMemo(() => {
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
if (sourceGroups.length > 0 && destinationResource) return false;
|
||||
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports, destinationResource]);
|
||||
}, [sourceGroups, destinationGroups, destinationResource]);
|
||||
|
||||
const submitDisabled = useMemo(() => {
|
||||
if (name.length == 0) return true;
|
||||
@@ -185,9 +185,11 @@ export function AccessControlModalContent({
|
||||
setProtocol(p);
|
||||
if (p == "icmp") {
|
||||
setPorts([]);
|
||||
setPortRanges([]);
|
||||
}
|
||||
if (p == "all") {
|
||||
setPorts([]);
|
||||
setPortRanges([]);
|
||||
}
|
||||
if (p == "tcp" || p == "udp") {
|
||||
setDirection("in");
|
||||
@@ -340,14 +342,16 @@ export function AccessControlModalContent({
|
||||
</Label>
|
||||
<HelpText>
|
||||
Allow network traffic and access only to specified ports.
|
||||
Select ports between 1 and 65535.
|
||||
Select ports or port ranges between 1 and 65535.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={""}>
|
||||
<PortSelector
|
||||
showAll={direction == "bi"}
|
||||
values={ports}
|
||||
onChange={setPorts}
|
||||
showAll={true}
|
||||
ports={ports}
|
||||
onPortsChange={setPorts}
|
||||
portRanges={portRanges}
|
||||
onPortRangesChange={setPortRanges}
|
||||
disabled={portAndDirectionDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,29 +5,45 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@components/Tooltip";
|
||||
import { orderBy } from "lodash";
|
||||
import React, { useMemo } from "react";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
|
||||
type Props = {
|
||||
policy: Policy;
|
||||
};
|
||||
export default function AccessControlPortsCell({ policy }: Props) {
|
||||
const firstRule = useMemo(() => {
|
||||
|
||||
export default function AccessControlPortsCell({ policy }: Readonly<Props>) {
|
||||
const rule = useMemo(() => {
|
||||
if (policy.rules.length > 0) return policy.rules[0];
|
||||
return undefined;
|
||||
}, [policy]);
|
||||
|
||||
const hasPorts = firstRule?.ports && firstRule?.ports.length > 0;
|
||||
const hasPorts = rule?.ports && rule?.ports?.length > 0;
|
||||
const hasPortRanges = rule?.port_ranges && rule?.port_ranges?.length > 0;
|
||||
const hasAnyPorts = hasPorts || hasPortRanges;
|
||||
|
||||
const allPorts = useMemo(() => {
|
||||
const ports = rule?.ports ?? [];
|
||||
const portRanges =
|
||||
rule?.port_ranges?.map((r) => {
|
||||
if (r.start === r.end) return `${r.start}`;
|
||||
return `${r.start}-${r.end}`;
|
||||
}) ?? [];
|
||||
return orderBy(
|
||||
[...portRanges, ...ports],
|
||||
[(p) => Number(p.split("-")[0])],
|
||||
["asc"],
|
||||
);
|
||||
}, [rule]);
|
||||
|
||||
const firstTwoPorts = useMemo(() => {
|
||||
if (!hasPorts) return [];
|
||||
return firstRule?.ports.slice(0, 2) ?? [];
|
||||
}, [hasPorts, firstRule]);
|
||||
return allPorts?.slice(0, 2) ?? [];
|
||||
}, [allPorts]);
|
||||
|
||||
const otherPorts = useMemo(() => {
|
||||
if (!hasPorts) return [];
|
||||
return firstRule?.ports.slice(2) ?? [];
|
||||
}, [hasPorts, firstRule]);
|
||||
return allPorts?.slice(2) ?? [];
|
||||
}, [allPorts]);
|
||||
|
||||
return (
|
||||
<div className={"flex-1"}>
|
||||
@@ -35,7 +51,7 @@ export default function AccessControlPortsCell({ policy }: Props) {
|
||||
<Tooltip delayDuration={1}>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<div className={"inline-flex items-center gap-2"}>
|
||||
{!hasPorts && (
|
||||
{!hasAnyPorts && (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
className={"uppercase tracking-wider font-medium"}
|
||||
@@ -44,20 +60,19 @@ export default function AccessControlPortsCell({ policy }: Props) {
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{firstTwoPorts &&
|
||||
firstTwoPorts.map((port) => {
|
||||
return (
|
||||
<Badge
|
||||
key={port}
|
||||
variant={"gray"}
|
||||
className={
|
||||
"px-3 gap-2 whitespace-nowrap uppercase tracking-wider font-medium"
|
||||
}
|
||||
>
|
||||
{port}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
{firstTwoPorts?.map((port) => {
|
||||
return (
|
||||
<Badge
|
||||
key={port}
|
||||
variant={"gray"}
|
||||
className={
|
||||
"px-3 gap-2 whitespace-nowrap uppercase tracking-wider font-medium"
|
||||
}
|
||||
>
|
||||
{port}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
|
||||
{otherPorts && otherPorts.length > 0 && (
|
||||
<Badge
|
||||
@@ -73,9 +88,17 @@ export default function AccessControlPortsCell({ policy }: Props) {
|
||||
</TooltipTrigger>
|
||||
{otherPorts && otherPorts.length > 0 && (
|
||||
<TooltipContent>
|
||||
<div className={"flex flex-col gap-2 items-start mt-3 mb-2"}>
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-start mt-3 mb-2 flex-wrap max-w-sm"
|
||||
}
|
||||
>
|
||||
{otherPorts.map((port) => {
|
||||
return <Badge key={port}>{port}</Badge>;
|
||||
return (
|
||||
<Badge key={port} variant={"gray"}>
|
||||
{port}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Policy, Protocol } from "@/interfaces/Policy";
|
||||
import { Policy, PortRange, Protocol } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { usePostureCheck } from "@/modules/posture-checks/usePostureCheck";
|
||||
@@ -83,6 +83,15 @@ export const useAccessControl = ({
|
||||
return [];
|
||||
});
|
||||
|
||||
const [portRanges, setPortRanges] = useState<PortRange[]>(() => {
|
||||
if (!firstRule) return [];
|
||||
if (firstRule.port_ranges == undefined) return [];
|
||||
if (firstRule.port_ranges.length > 0) {
|
||||
return firstRule.port_ranges;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const [protocol, setProtocol] = useState<Protocol>(
|
||||
firstRule ? firstRule.protocol : "all",
|
||||
);
|
||||
@@ -139,6 +148,11 @@ export const useAccessControl = ({
|
||||
destinations = tmp;
|
||||
}
|
||||
|
||||
const [newPorts, newPortRanges] = parseAccessControlPorts(
|
||||
ports,
|
||||
portRanges,
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
@@ -155,7 +169,8 @@ export const useAccessControl = ({
|
||||
action: "accept",
|
||||
protocol,
|
||||
enabled,
|
||||
ports: ports.length > 0 ? ports.map((p) => p.toString()) : undefined,
|
||||
ports: newPorts,
|
||||
port_ranges: newPortRanges,
|
||||
},
|
||||
],
|
||||
} as Policy;
|
||||
@@ -206,6 +221,11 @@ export const useAccessControl = ({
|
||||
destinations = tmp;
|
||||
}
|
||||
|
||||
const [newPorts, newPortRanges] = parseAccessControlPorts(
|
||||
ports,
|
||||
portRanges,
|
||||
);
|
||||
|
||||
const policyObj = {
|
||||
name,
|
||||
description,
|
||||
@@ -224,7 +244,8 @@ export const useAccessControl = ({
|
||||
sources,
|
||||
destinations: destinationResource ? undefined : destinations,
|
||||
destinationResource: destinationResource || undefined,
|
||||
ports: ports.length > 0 ? ports.map((p) => p.toString()) : undefined,
|
||||
ports: newPorts,
|
||||
port_ranges: newPortRanges,
|
||||
},
|
||||
],
|
||||
} as Policy;
|
||||
@@ -267,6 +288,8 @@ export const useAccessControl = ({
|
||||
setEnabled,
|
||||
ports,
|
||||
setPorts,
|
||||
portRanges,
|
||||
setPortRanges,
|
||||
sourceGroups,
|
||||
setSourceGroups,
|
||||
destinationGroups,
|
||||
@@ -281,3 +304,19 @@ export const useAccessControl = ({
|
||||
setDestinationResource,
|
||||
} as const;
|
||||
};
|
||||
|
||||
const parseAccessControlPorts = (ports: number[], portRanges: PortRange[]) => {
|
||||
const hasRanges = portRanges.length > 0;
|
||||
const hasPorts = ports.length > 0;
|
||||
if (!hasPorts && !hasRanges) return [undefined, undefined];
|
||||
if (!hasRanges) return [ports.map(String), undefined];
|
||||
if (!hasPorts) return [undefined, portRanges];
|
||||
|
||||
const portRangesFromPorts = ports.map((port) => ({
|
||||
start: port,
|
||||
end: port,
|
||||
})) as PortRange[];
|
||||
|
||||
const allRanges = [...portRanges, ...portRangesFromPorts];
|
||||
return [undefined, allRanges];
|
||||
};
|
||||
@@ -314,6 +314,7 @@ function RoutingPeerModalContent({
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
disabled={isNonLinuxRoutingPeer}
|
||||
routingPeerGroupId={routingPeerGroups?.[0]?.id}
|
||||
/>
|
||||
|
||||
<div className={cn("flex justify-between")}>
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
import { Callout } from "@components/Callout";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { VenetianMask } from "lucide-react";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { AlertCircleIcon, VenetianMask } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { GroupPeer } from "@/interfaces/Group";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
|
||||
type Props = {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
routingPeerGroupId?: string;
|
||||
};
|
||||
export const RoutingPeerMasqueradeSwitch = ({
|
||||
disabled = false,
|
||||
value,
|
||||
onChange,
|
||||
routingPeerGroupId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<RoutingPeerMasqueradeTooltip show={disabled}>
|
||||
<FancyToggleSwitch
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<>
|
||||
<VenetianMask size={15} />
|
||||
Masquerade
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow access to your private networks without configuring routes on your local routers or other devices."
|
||||
}
|
||||
/>
|
||||
<div className={"flex flex-col gap-4"}>
|
||||
<FancyToggleSwitch
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<>
|
||||
<VenetianMask size={15} />
|
||||
Masquerade
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow access to your private networks without configuring routes on your local routers or other devices."
|
||||
}
|
||||
/>
|
||||
{routingPeerGroupId && !value && (
|
||||
<RoutingPeerGroupNonLinuxWarning
|
||||
routingPeerGroupId={routingPeerGroupId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</RoutingPeerMasqueradeTooltip>
|
||||
);
|
||||
};
|
||||
@@ -52,9 +69,51 @@ export const RoutingPeerMasqueradeTooltip = ({
|
||||
delayDuration={250}
|
||||
skipDelayDuration={350}
|
||||
disabled={!show}
|
||||
className={"cursor-help"}
|
||||
className={cn(show && "cursor-help")}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const RoutingPeerGroupNonLinuxWarning = ({
|
||||
routingPeerGroupId,
|
||||
}: {
|
||||
routingPeerGroupId: string;
|
||||
}) => {
|
||||
const { groups } = useGroups();
|
||||
const { data: peers } = useFetchApi<Peer[]>("/peers", true);
|
||||
const group = groups?.find((g) => g.id === routingPeerGroupId);
|
||||
|
||||
const hasNonLinuxPeer = React.useMemo(() => {
|
||||
try {
|
||||
return group?.peers?.some((groupPeer) => {
|
||||
const peer = peers?.find((p) => p.id === (groupPeer as GroupPeer).id);
|
||||
if (!peer) return false;
|
||||
const os = getOperatingSystem(peer.os);
|
||||
return os !== OperatingSystem.LINUX;
|
||||
});
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}, [group?.peers, peers]);
|
||||
|
||||
return (
|
||||
hasNonLinuxPeer && (
|
||||
<Callout
|
||||
variant={"warning"}
|
||||
icon={
|
||||
<AlertCircleIcon
|
||||
size={14}
|
||||
className={"shrink-0 relative top-[3px] text-netbird"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Group <span className={"text-netbird font-normal"}>{group?.name}</span>{" "}
|
||||
contains at least one non-Linux peer.
|
||||
<br /> Disabled Masquerade will have no effect on non-Linux routing
|
||||
peers.
|
||||
</Callout>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -723,6 +723,7 @@ export function RouteModalContent({
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
disabled={isNonLinuxRoutingPeer}
|
||||
routingPeerGroupId={routingPeerGroups?.[0]?.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -461,6 +461,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
disabled={isNonLinuxRoutingPeer}
|
||||
routingPeerGroupId={routingPeerGroups?.[0]?.id}
|
||||
/>
|
||||
)}
|
||||
<div className={cn("flex justify-between")}>
|
||||
|
||||
@@ -24,7 +24,8 @@ export function removeAllSpaces(str: string) {
|
||||
return str.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
export const generateColorFromString = (str: string) => {
|
||||
export const generateColorFromString = (str?: string) => {
|
||||
if (!str) return "#f68330";
|
||||
if (str.includes("System")) return "#808080";
|
||||
if (str.toLowerCase().startsWith("netbird")) return "#f68330";
|
||||
let hash = 0;
|
||||
|
||||
+3
-1
@@ -14,6 +14,7 @@ const config: Config = {
|
||||
"50": "#f4f6f7",
|
||||
"100": "#e4e7e9",
|
||||
"200": "#cbd2d6",
|
||||
"250": "#b7c0c6",
|
||||
"300": "#a7b1b9",
|
||||
"350": "#8f9ca8",
|
||||
"400": "#7c8994",
|
||||
@@ -34,7 +35,8 @@ const config: Config = {
|
||||
DEFAULT: "#f68330",
|
||||
"50": "#fff6ed",
|
||||
"100": "#feecd6",
|
||||
"200": "#fcd5ac",
|
||||
"150": "#ffdfb8",
|
||||
"200": "#ffd4a6",
|
||||
"300": "#fab677",
|
||||
"400": "#f68330",
|
||||
"500": "#f46d1b",
|
||||
|
||||
Reference in new issue
Block a user