Compare commits

..
15 changed files with 478 additions and 157 deletions

No files matched your search

+6 -2
View File
@@ -1,11 +1,15 @@
import { cn } from "@utils/helpers";
import * as React from "react";
type Props = {
children: React.ReactNode;
className?: string;
};
export const DropdownInfoText = ({ children }: Props) => {
export const DropdownInfoText = ({ children, className }: Props) => {
return (
<div className={"text-center pt-2 mb-6 text-nb-gray-400"}>{children}</div>
<div className={cn("text-center pt-2 mb-6 text-nb-gray-400", className)}>
{children}
</div>
);
};
File diff suppressed because it is too large. Load diff
+71
View File
@@ -0,0 +1,71 @@
import * as RadioPrimitive from "@radix-ui/react-radio-group";
import { cn } from "@utils/helpers";
import { cva, VariantProps } from "class-variance-authority";
import * as React from "react";
import { forwardRef } from "react";
type RadioVariants = VariantProps<typeof variants>;
const variants = cva([], {
variants: {
variant: {
default: [
"dark:data-[state=unchecked]:bg-nb-gray-950 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
"dark:data-[state=checked]:bg-netbird",
],
},
},
});
const Radio = forwardRef<
React.ElementRef<typeof RadioPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioPrimitive.Root> & RadioVariants
>(
(
{ className, children, variant = "default", defaultValue, ...props },
ref,
) => (
<RadioPrimitive.Root
ref={ref}
defaultValue={defaultValue}
name={props.name}
{...props}
>
{children}
</RadioPrimitive.Root>
),
);
Radio.displayName = RadioPrimitive.Root.displayName;
type Props = {
value: string;
className?: string;
} & RadioVariants;
const RadioItem = ({ value, className, variant = "default" }: Props) => {
return (
<RadioPrimitive.Item
value={value}
className={cn(
variants({ variant }),
"border-neutral-900",
"peer h-5 w-5 shrink-0 rounded-full border",
"ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50 relative",
className,
)}
>
<RadioPrimitive.Indicator asChild={true}>
<div
className={cn(
"h-2 w-2 bg-netbird absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center rounded-full",
"data-[state=checked]:bg-white data-[state=checked]:text-neutral-50 ",
)}
></div>
</RadioPrimitive.Indicator>
</RadioPrimitive.Item>
);
};
RadioItem.displayName = RadioPrimitive.Item.displayName;
export { Radio, RadioItem };
+15 -3
View File
@@ -11,13 +11,15 @@ type Props<T extends { id?: string }> = {
items: T[];
onSelect: (item: T) => void;
renderItem?: (item: T) => React.ReactNode;
itemClassName?: string;
};
export function VirtualScrollAreaList<T extends { id?: string }>({
items,
onSelect,
renderItem,
}: Props<T>) {
itemClassName,
}: Readonly<Props<T>>) {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const [selected, setSelected] = useState(0);
@@ -81,8 +83,9 @@ export function VirtualScrollAreaList<T extends { id?: string }>({
<VirtualScrollListItemWrapper
onMouseEnter={() => setSelected(index)}
id={option.id}
onClick={() => onClick(option as T)}
onClick={() => onClick(option)}
ariaSelected={selected === index}
className={itemClassName}
>
{renderMemoizedItem ? renderMemoizedItem(option) : option.id}
</VirtualScrollListItemWrapper>
@@ -103,10 +106,18 @@ type ItemWrapperProps = {
onMouseEnter?: () => void;
onClick?: () => void;
ariaSelected?: boolean;
className?: string;
};
export const VirtualScrollListItemWrapper = memo(
({ id, children, onClick, onMouseEnter, ariaSelected }: ItemWrapperProps) => {
({
id,
children,
onClick,
onMouseEnter,
ariaSelected,
className,
}: ItemWrapperProps) => {
return (
<div
key={id ?? undefined}
@@ -118,6 +129,7 @@ export const VirtualScrollListItemWrapper = memo(
className={cn(
"text-xs flex justify-between py-2 px-3 cursor-pointer items-center rounded-md",
"bg-transparent dark:aria-selected:bg-nb-gray-800/50",
className,
)}
aria-selected={ariaSelected}
role={"listitem"}
+58
View File
@@ -0,0 +1,58 @@
import Badge from "@components/Badge";
import TruncatedText from "@components/ui/TruncatedText";
import { cn } from "@utils/helpers";
import { GlobeIcon, NetworkIcon, WorkflowIcon, XIcon } from "lucide-react";
import * as React from "react";
import { NetworkResource } from "@/interfaces/Network";
type Props = {
resource?: NetworkResource;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
showX?: boolean;
children?: React.ReactNode;
className?: string;
};
export default function ResourceBadge({
onClick,
resource,
showX = false,
children,
className,
}: Readonly<Props>) {
if (!resource) return;
return (
<Badge
key={resource.id || resource?.name}
useHover={true}
data-cy={"resource-badge"}
variant={"gray-ghost"}
className={cn("transition-all group whitespace-nowrap", className)}
onClick={(e) => {
e.preventDefault();
onClick?.(e);
}}
>
{resource.type === "host" && (
<WorkflowIcon size={12} className={"shrink-0"} />
)}
{resource.type === "domain" && (
<GlobeIcon size={12} className={"shrink-0"} />
)}
{resource.type === "subnet" && (
<NetworkIcon size={12} className={"shrink-0"} />
)}
<TruncatedText text={resource?.name || ""} maxChars={20} />
{children}
{showX && (
<XIcon
size={12}
className={
"cursor-pointer group-hover:text-nb-gray-100 transition-all shrink-0"
}
/>
)}
</Badge>
);
}
+7
View File
@@ -22,6 +22,13 @@ export interface PolicyRule {
action: string;
protocol: Protocol;
ports: string[];
sourceResource?: PolicyRuleResource;
destinationResource?: PolicyRuleResource;
}
export interface PolicyRuleResource {
id: string;
type: "domain" | "host" | "subnet" | undefined;
}
export type Protocol = "all" | "tcp" | "udp" | "icmp";
@@ -149,6 +149,8 @@ export function AccessControlModalContent({
submit,
isPostureChecksLoading,
getPolicyData,
destinationResource,
setDestinationResource,
} = useAccessControl({
policy,
postureCheckTemplates,
@@ -166,9 +168,10 @@ export function AccessControlModalContent({
});
const continuePostureChecksDisabled = useMemo(() => {
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
if (direction != "bi" && ports.length == 0) return true;
}, [sourceGroups, destinationGroups, direction, ports]);
if (sourceGroups.length > 0 && destinationResource) return false;
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
}, [sourceGroups, destinationGroups, direction, ports, destinationResource]);
const submitDisabled = useMemo(() => {
if (name.length == 0) return true;
@@ -304,6 +307,10 @@ export function AccessControlModalContent({
onChange={setDestinationGroups}
values={destinationGroups}
saveGroupAssignments={useSave}
resource={destinationResource}
onResourceChange={setDestinationResource}
showResources={true}
placeholder={"Select destination(s)..."}
/>
</div>
</div>
@@ -32,6 +32,9 @@ export default function AccessControlActiveCell({ policy }: Readonly<Props>) {
return group.id;
}) as string[])
: [];
if (rule.destinationResource) {
rule.destinations = null;
}
});
updatePolicy(
@@ -1,18 +1,49 @@
import MultipleGroups from "@components/ui/MultipleGroups";
import ResourceBadge from "@components/ui/ResourceBadge";
import useFetchApi from "@utils/api";
import React, { useMemo } from "react";
import Skeleton from "react-loading-skeleton";
import { Group } from "@/interfaces/Group";
import { Policy } from "@/interfaces/Policy";
import { NetworkResource } from "@/interfaces/Network";
import { Policy, PolicyRuleResource } from "@/interfaces/Policy";
type Props = {
policy: Policy;
};
export default function AccessControlDestinationsCell({ policy }: Props) {
export default function AccessControlDestinationsCell({
policy,
}: Readonly<Props>) {
const firstRule = useMemo(() => {
if (policy.rules.length > 0) return policy.rules[0];
return undefined;
}, [policy]);
if (firstRule?.destinationResource) {
return (
<AccessControlDestinationResourceCell
resource={firstRule.destinationResource}
/>
);
}
return firstRule ? (
<MultipleGroups groups={firstRule.destinations as Group[]} />
) : null;
}
const AccessControlDestinationResourceCell = ({
resource,
}: {
resource: PolicyRuleResource;
}) => {
const { data: resources, isLoading } = useFetchApi<NetworkResource[]>(
"/networks/resources",
);
if (isLoading) return <Skeleton height={35} width={"50%"} />;
return (
<div className={"flex"}>
<ResourceBadge resource={resources?.find((r) => r.id === resource.id)} />
</div>
);
};
+10 -2
View File
@@ -117,6 +117,10 @@ export const useAccessControl = ({
: initialDestinationGroups ?? [],
});
const [destinationResource, setDestinationResource] = useState(
firstRule?.destinationResource,
);
const { updateOrCreateAndNotify: checkToCreate } = usePostureCheck({});
const createPostureChecksWithoutID = async () => {
const checks = postureChecks.filter(
@@ -146,7 +150,8 @@ export const useAccessControl = ({
description,
name,
sources: sources,
destinations: destinations,
destinations: destinationResource ? undefined : destinations,
destinationResource: destinationResource || undefined,
action: "accept",
protocol,
enabled,
@@ -214,7 +219,8 @@ export const useAccessControl = ({
protocol,
enabled,
sources,
destinations,
destinations: destinationResource ? undefined : destinations,
destinationResource: destinationResource || undefined,
ports: ports.length > 0 ? ports.map((p) => p.toString()) : undefined,
},
],
@@ -268,5 +274,7 @@ export const useAccessControl = ({
getPolicyData,
portAndDirectionDisabled,
isPostureChecksLoading,
destinationResource,
setDestinationResource,
} as const;
};
@@ -17,7 +17,6 @@ import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { Textarea } from "@components/Textarea";
import { useApiCall } from "@utils/api";
import {
ExternalLinkIcon,
@@ -157,18 +156,6 @@ export function ResourceModalContent({
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<Label>Description (optional)</Label>
<HelpText>
Write a short description to add more context to this resource.
</HelpText>
<Textarea
placeholder={"e.g., Production, Development"}
value={description}
rows={1}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<ResourceSingleAddressInput value={address} onChange={setAddress} />
@@ -34,7 +34,7 @@ export default function ResourceNameCell({ resource }: Readonly<Props>) {
<div
className={cn(
"flex flex-col gap-0 text-neutral-300 font-light truncate",
"group-hover:text-neutral-100 text-left",
"group-hover:text-neutral-100",
)}
>
<TextWithTooltip
@@ -43,7 +43,6 @@ export default function ResourceNameCell({ resource }: Readonly<Props>) {
className={"font-normal"}
/>
<DescriptionWithTooltip
maxChars={25}
className={cn("font-normal mt-0.5 ")}
text={resource.description}
/>
@@ -22,12 +22,18 @@ export const ResourcePolicyCell = ({ resource }: Props) => {
const resourceGroups = resource?.groups as Group[];
return policies?.filter((policy) => {
if (!policy.enabled) return false;
const destinationResource = policy.rules
?.map((rule) => rule?.destinationResource?.id === resource?.id)
.some((id) => id);
if (destinationResource) return true;
const destinationPolicyGroups = policy.rules
?.map((rule) => rule?.destinations)
.flat() as Group[];
const policyGroups = [...destinationPolicyGroups];
return resourceGroups.some((resourceGroup) =>
policyGroups.some((policyGroup) => policyGroup.id === resourceGroup.id),
policyGroups.some(
(policyGroup) => policyGroup?.id === resourceGroup.id,
),
);
});
}, [policies, resource]);
@@ -6,7 +6,6 @@ import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
import { IconCirclePlus } from "@tabler/icons-react";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { removeAllSpaces } from "@utils/helpers";
import { Layers3Icon } from "lucide-react";
import * as React from "react";
import { useState } from "react";
@@ -37,12 +36,6 @@ const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
return <ResourceNameCell resource={row.original} />;
},
},
{
id: "description",
accessorKey: "description",
accessorFn: (resource) =>
removeAllSpaces(resource?.description || "").toLowerCase(),
},
{
id: "address",
accessorKey: "address",
@@ -129,9 +122,7 @@ export default function ResourcesTable({
icon={<Layers3Icon size={20} />}
/>
}
columnVisibility={{
description: false,
}}
columnVisibility={{}}
paginationPaddingClassName={"px-0 pt-8"}
rightSide={() => (
<Button
-1
View File
@@ -100,7 +100,6 @@ export default function RouteModal({ children, open, setOpen }: Props) {
},
],
};
console.log(newPolicy);
setNewPolicy(newPolicy);
setRoutePolicyModal(true);
};