mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3894eb98a8 | ||
|
|
ca88b04c9e | ||
|
|
baf6324ee7 | ||
|
|
ec3fc8d1d5 | ||
|
|
b20496edeb |
No files matched your search
@@ -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
@@ -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 };
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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]);
|
||||
|
||||
@@ -100,7 +100,6 @@ export default function RouteModal({ children, open, setOpen }: Props) {
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log(newPolicy);
|
||||
setNewPolicy(newPolicy);
|
||||
setRoutePolicyModal(true);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user