mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Add posture checks to further restrict network access (#338)
This commit is contained in:
@@ -28,9 +28,10 @@ export default function AccessControlPage() {
|
||||
<Breadcrumbs.Item
|
||||
href={"/policies"}
|
||||
label={"Access Control"}
|
||||
icon={<AccessControlIcon size={13} />}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
<h1>
|
||||
{policies && policies.length > 1
|
||||
? `${policies.length} Access Control Policies`
|
||||
|
||||
@@ -339,10 +339,12 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
</>
|
||||
}
|
||||
value={
|
||||
dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
")"
|
||||
peer.connected
|
||||
? "just now"
|
||||
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
<Card.ListItem
|
||||
|
||||
8
src/app/(dashboard)/posture-checks/layout.tsx
Normal file
8
src/app/(dashboard)/posture-checks/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Posture Checks - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
72
src/app/(dashboard)/posture-checks/page.tsx
Normal file
72
src/app/(dashboard)/posture-checks/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const PostureCheckTable = lazy(
|
||||
() => import("@/modules/posture-checks/table/PostureCheckTable"),
|
||||
);
|
||||
export default function PostureChecksPage() {
|
||||
const { data: postureChecks, isLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/access-control"}
|
||||
label={"Access Control"}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/posture-checks"}
|
||||
label={"Posture Checks"}
|
||||
active
|
||||
icon={<ShieldCheck size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>
|
||||
{postureChecks && postureChecks.length > 1
|
||||
? `${postureChecks.length} Posture Checks`
|
||||
: "Posture Checks"}
|
||||
</h1>
|
||||
<Paragraph>
|
||||
Use posture checks to further restrict access in your network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink href={"#"} target={"_blank"}>
|
||||
Posture Checks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess page={"Posture Checks"}>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PostureCheckTable
|
||||
isLoading={isLoading}
|
||||
postureChecks={postureChecks}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</RestrictedAccess>
|
||||
</GroupsProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
20
src/assets/countries/CountryDERounded.tsx
Normal file
20
src/assets/countries/CountryDERounded.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import deIcon from "@/assets/countries/de.svg";
|
||||
|
||||
export const CountryDERounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={deIcon}
|
||||
alt={"de"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
src/assets/countries/RoundedFlag.tsx
Normal file
30
src/assets/countries/RoundedFlag.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
type Props = {
|
||||
country: string;
|
||||
size?: number;
|
||||
};
|
||||
const RoundedFlag = ({ country, size = 20 }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"shrink-0 overflow-hidden rounded-full relative shadow-xl flex items-center justify-center"
|
||||
}
|
||||
style={{
|
||||
width: size == 14 ? 20 : size,
|
||||
height: size == 14 ? 20 : size,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`/assets/flags/4x3/${country.toLowerCase()}.svg`}
|
||||
alt={country}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RoundedFlag);
|
||||
9
src/assets/countries/de.svg
Normal file
9
src/assets/countries/de.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" viewBox="0 0 5 3">
|
||||
<desc>Flag of Germany</desc>
|
||||
<rect id="black_stripe" width="5" height="3" y="0" x="0" fill="#000"/>
|
||||
<rect id="red_stripe" width="5" height="2" y="1" x="0" fill="#D00"/>
|
||||
<rect id="gold_stripe" width="5" height="1" y="2" x="0" fill="#FFCE00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
65
src/assets/icons/LinuxIcon.tsx
Normal file
65
src/assets/icons/LinuxIcon.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as React from "react";
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export const LinuxIcon = (props: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="800px"
|
||||
width="800px"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 304.998 304.998"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<g id="XMLID_91_">
|
||||
<path
|
||||
id="XMLID_92_"
|
||||
d="M274.659,244.888c-8.944-3.663-12.77-8.524-12.4-15.777c0.381-8.466-4.422-14.667-6.703-17.117
|
||||
c1.378-5.264,5.405-23.474,0.004-39.291c-5.804-16.93-23.524-42.787-41.808-68.204c-7.485-10.438-7.839-21.784-8.248-34.922
|
||||
c-0.392-12.531-0.834-26.735-7.822-42.525C190.084,9.859,174.838,0,155.851,0c-11.295,0-22.889,3.53-31.811,9.684
|
||||
c-18.27,12.609-15.855,40.1-14.257,58.291c0.219,2.491,0.425,4.844,0.545,6.853c1.064,17.816,0.096,27.206-1.17,30.06
|
||||
c-0.819,1.865-4.851,7.173-9.118,12.793c-4.413,5.812-9.416,12.4-13.517,18.539c-4.893,7.387-8.843,18.678-12.663,29.597
|
||||
c-2.795,7.99-5.435,15.537-8.005,20.047c-4.871,8.676-3.659,16.766-2.647,20.505c-1.844,1.281-4.508,3.803-6.757,8.557
|
||||
c-2.718,5.8-8.233,8.917-19.701,11.122c-5.27,1.078-8.904,3.294-10.804,6.586c-2.765,4.791-1.259,10.811,0.115,14.925
|
||||
c2.03,6.048,0.765,9.876-1.535,16.826c-0.53,1.604-1.131,3.42-1.74,5.423c-0.959,3.161-0.613,6.035,1.026,8.542
|
||||
c4.331,6.621,16.969,8.956,29.979,10.492c7.768,0.922,16.27,4.029,24.493,7.035c8.057,2.944,16.388,5.989,23.961,6.913
|
||||
c1.151,0.145,2.291,0.218,3.39,0.218c11.434,0,16.6-7.587,18.238-10.704c4.107-0.838,18.272-3.522,32.871-3.882
|
||||
c14.576-0.416,28.679,2.462,32.674,3.357c1.256,2.404,4.567,7.895,9.845,10.724c2.901,1.586,6.938,2.495,11.073,2.495
|
||||
c0.001,0,0,0,0.001,0c4.416,0,12.817-1.044,19.466-8.039c6.632-7.028,23.202-16,35.302-22.551c2.7-1.462,5.226-2.83,7.441-4.065
|
||||
c6.797-3.768,10.506-9.152,10.175-14.771C282.445,250.905,279.356,246.811,274.659,244.888z M124.189,243.535
|
||||
c-0.846-5.96-8.513-11.871-17.392-18.715c-7.26-5.597-15.489-11.94-17.756-17.312c-4.685-11.082-0.992-30.568,5.447-40.602
|
||||
c3.182-5.024,5.781-12.643,8.295-20.011c2.714-7.956,5.521-16.182,8.66-19.783c4.971-5.622,9.565-16.561,10.379-25.182
|
||||
c4.655,4.444,11.876,10.083,18.547,10.083c1.027,0,2.024-0.134,2.977-0.403c4.564-1.318,11.277-5.197,17.769-8.947
|
||||
c5.597-3.234,12.499-7.222,15.096-7.585c4.453,6.394,30.328,63.655,32.972,82.044c2.092,14.55-0.118,26.578-1.229,31.289
|
||||
c-0.894-0.122-1.96-0.221-3.08-0.221c-7.207,0-9.115,3.934-9.612,6.283c-1.278,6.103-1.413,25.618-1.427,30.003
|
||||
c-2.606,3.311-15.785,18.903-34.706,21.706c-7.707,1.12-14.904,1.688-21.39,1.688c-5.544,0-9.082-0.428-10.551-0.651l-9.508-10.879
|
||||
C121.429,254.489,125.177,250.583,124.189,243.535z M136.254,64.149c-0.297,0.128-0.589,0.265-0.876,0.411
|
||||
c-0.029-0.644-0.096-1.297-0.199-1.952c-1.038-5.975-5-10.312-9.419-10.312c-0.327,0-0.656,0.025-1.017,0.08
|
||||
c-2.629,0.438-4.691,2.413-5.821,5.213c0.991-6.144,4.472-10.693,8.602-10.693c4.85,0,8.947,6.536,8.947,14.272
|
||||
C136.471,62.143,136.4,63.113,136.254,64.149z M173.94,68.756c0.444-1.414,0.684-2.944,0.684-4.532
|
||||
c0-7.014-4.45-12.509-10.131-12.509c-5.552,0-10.069,5.611-10.069,12.509c0,0.47,0.023,0.941,0.067,1.411
|
||||
c-0.294-0.113-0.581-0.223-0.861-0.329c-0.639-1.935-0.962-3.954-0.962-6.015c0-8.387,5.36-15.211,11.95-15.211
|
||||
c6.589,0,11.95,6.824,11.95,15.211C176.568,62.78,175.605,66.11,173.94,68.756z M169.081,85.08
|
||||
c-0.095,0.424-0.297,0.612-2.531,1.774c-1.128,0.587-2.532,1.318-4.289,2.388l-1.174,0.711c-4.718,2.86-15.765,9.559-18.764,9.952
|
||||
c-2.037,0.274-3.297-0.516-6.13-2.441c-0.639-0.435-1.319-0.897-2.044-1.362c-5.107-3.351-8.392-7.042-8.763-8.485
|
||||
c1.665-1.287,5.792-4.508,7.905-6.415c4.289-3.988,8.605-6.668,10.741-6.668c0.113,0,0.215,0.008,0.321,0.028
|
||||
c2.51,0.443,8.701,2.914,13.223,4.718c2.09,0.834,3.895,1.554,5.165,2.01C166.742,82.664,168.828,84.422,169.081,85.08z
|
||||
M205.028,271.45c2.257-10.181,4.857-24.031,4.436-32.196c-0.097-1.855-0.261-3.874-0.42-5.826
|
||||
c-0.297-3.65-0.738-9.075-0.283-10.684c0.09-0.042,0.19-0.078,0.301-0.109c0.019,4.668,1.033,13.979,8.479,17.226
|
||||
c2.219,0.968,4.755,1.458,7.537,1.458c7.459,0,15.735-3.659,19.125-7.049c1.996-1.996,3.675-4.438,4.851-6.372
|
||||
c0.257,0.753,0.415,1.737,0.332,3.005c-0.443,6.885,2.903,16.019,9.271,19.385l0.927,0.487c2.268,1.19,8.292,4.353,8.389,5.853
|
||||
c-0.001,0.001-0.051,0.177-0.387,0.489c-1.509,1.379-6.82,4.091-11.956,6.714c-9.111,4.652-19.438,9.925-24.076,14.803
|
||||
c-6.53,6.872-13.916,11.488-18.376,11.488c-0.537,0-1.026-0.068-1.461-0.206C206.873,288.406,202.886,281.417,205.028,271.45z
|
||||
M39.917,245.477c-0.494-2.312-0.884-4.137-0.465-5.905c0.304-1.31,6.771-2.714,9.533-3.313c3.883-0.843,7.899-1.714,10.525-3.308
|
||||
c3.551-2.151,5.474-6.118,7.17-9.618c1.228-2.531,2.496-5.148,4.005-6.007c0.085-0.05,0.215-0.108,0.463-0.108
|
||||
c2.827,0,8.759,5.943,12.177,11.262c0.867,1.341,2.473,4.028,4.331,7.139c5.557,9.298,13.166,22.033,17.14,26.301
|
||||
c3.581,3.837,9.378,11.214,7.952,17.541c-1.044,4.909-6.602,8.901-7.913,9.784c-0.476,0.108-1.065,0.163-1.758,0.163
|
||||
c-7.606,0-22.662-6.328-30.751-9.728l-1.197-0.503c-4.517-1.894-11.891-3.087-19.022-4.241c-5.674-0.919-13.444-2.176-14.732-3.312
|
||||
c-1.044-1.171,0.167-4.978,1.235-8.337c0.769-2.414,1.563-4.91,1.998-7.523C41.225,251.596,40.499,248.203,39.917,245.477z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const auth0AuthorityConfig: AuthorityConfiguration = {
|
||||
|
||||
const onEvent = (configurationName: any, eventName: any, data: any) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.info(`oidc:${configurationName}:${eventName}`, data);
|
||||
//console.info(`oidc:${configurationName}:${eventName}`, data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
90
src/components/AutoCompleteInput.tsx
Normal file
90
src/components/AutoCompleteInput.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { Input } from "@components/Input";
|
||||
import { Popover, PopoverContent } from "@components/Popover";
|
||||
import { useElementSize } from "@hooks/useElementSize";
|
||||
import { Anchor } from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
|
||||
type Props = {};
|
||||
export const AutoCompleteInput = ({}: Props) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [elementWidth, { width }] = useElementSize<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const input = inputRef.current;
|
||||
|
||||
const onFocus = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
if (input) {
|
||||
inputRef.current.addEventListener("focus", onFocus);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
inputRef.current.removeEventListener("focus", onFocus);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"z-10 relative"}>
|
||||
<Popover modal={false} open={open} onOpenChange={setOpen}>
|
||||
<Anchor ref={elementWidth}>
|
||||
<Input
|
||||
placeholder={"11"}
|
||||
ref={inputRef}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
customPrefix={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Checkbox></Checkbox>
|
||||
<div
|
||||
className={"flex gap-2 items-center text-sm text-nb-gray-200"}
|
||||
>
|
||||
<FaWindows className={"text-sky-600 text-lg"} />
|
||||
Windows
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Anchor>
|
||||
|
||||
<PopoverContent
|
||||
hideWhenDetached={false}
|
||||
className="w-full p-0 shadow-sm shadow-nb-gray-950"
|
||||
style={{
|
||||
width: width,
|
||||
}}
|
||||
forceMount={true}
|
||||
align="start"
|
||||
side={"bottom"}
|
||||
sideOffset={10}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
onInteractOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onFocusOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
></PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,21 +9,23 @@ const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-950",
|
||||
"peer h-5 w-5 shrink-0 rounded-[4px] border border-neutral-900 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 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-netbird dark:data-[state=checked]:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
<div className={"h-5 w-5"}>
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-950",
|
||||
"peer h-5 w-5 shrink-0 rounded-[4px] border border-neutral-900 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 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-netbird dark:data-[state=checked]:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={"flex items-center justify-center"}
|
||||
>
|
||||
<Check size={14} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
</div>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
|
||||
@@ -5,16 +5,20 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@components/Tooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
hoverButton?: boolean;
|
||||
isAction?: boolean;
|
||||
interactive?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
align?: "end" | "center" | "start";
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
keepOpen?: boolean;
|
||||
};
|
||||
export default function FullTooltip({
|
||||
children,
|
||||
@@ -24,26 +28,45 @@ export default function FullTooltip({
|
||||
interactive = true,
|
||||
disabled,
|
||||
className,
|
||||
contentClassName,
|
||||
align = "center",
|
||||
side = "top",
|
||||
keepOpen = false,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(!!keepOpen);
|
||||
|
||||
const handleOpen = (isOpen: boolean) => {
|
||||
if (keepOpen) return;
|
||||
setOpen(isOpen);
|
||||
};
|
||||
|
||||
return !disabled ? (
|
||||
<TooltipProvider disableHoverableContent={!interactive}>
|
||||
<Tooltip delayDuration={1}>
|
||||
<TooltipTrigger asChild={true}>
|
||||
{hoverButton ? (
|
||||
<div
|
||||
className={cn(
|
||||
isAction ? "cursor-pointer" : "cursor-default",
|
||||
"inline-flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("inline-flex", className)}>{children}</div>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<Tooltip delayDuration={1} open={open} onOpenChange={handleOpen}>
|
||||
{children && (
|
||||
<TooltipTrigger asChild={true}>
|
||||
{hoverButton ? (
|
||||
<div
|
||||
className={cn(
|
||||
isAction ? "cursor-pointer" : "cursor-default",
|
||||
"inline-flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("inline-flex", className)}>{children}</div>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
{!disabled && (
|
||||
<TooltipContent alignOffset={20}>
|
||||
<TooltipContent
|
||||
alignOffset={20}
|
||||
forceMount={true}
|
||||
className={contentClassName}
|
||||
align={align}
|
||||
side={side}
|
||||
>
|
||||
<div className={"text-neutral-300 flex flex-col gap-1"}>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export interface InputProps
|
||||
@@ -10,6 +12,7 @@ export interface InputProps
|
||||
maxWidthClass?: string;
|
||||
icon?: React.ReactNode;
|
||||
error?: string;
|
||||
errorTooltip?: boolean;
|
||||
}
|
||||
|
||||
const inputVariants = cva("", {
|
||||
@@ -45,6 +48,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
icon,
|
||||
maxWidthClass = "",
|
||||
error,
|
||||
errorTooltip = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -60,7 +64,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
}),
|
||||
"flex h-[42px] w-auto rounded-l-md bg-white px-3 py-2 text-sm ",
|
||||
"border items-center whitespace-nowrap",
|
||||
props.disabled && "opacity-50",
|
||||
props.disabled && "opacity-20",
|
||||
)}
|
||||
>
|
||||
{customPrefix}
|
||||
@@ -99,8 +103,33 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
>
|
||||
{customSuffix}
|
||||
</div>
|
||||
{error && errorTooltip && (
|
||||
<div
|
||||
className={
|
||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
|
||||
}
|
||||
>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs text-red-500 inline-flex"}>
|
||||
<AlertCircle
|
||||
size={13}
|
||||
className={"top-[1px] relative mr-2"}
|
||||
/>
|
||||
{error}
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
align={"center"}
|
||||
side={"top"}
|
||||
keepOpen={true}
|
||||
>
|
||||
|
||||
</FullTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
{error && !errorTooltip && (
|
||||
<Paragraph className={"text-xs !text-red-500 mt-2"}>
|
||||
{error}
|
||||
</Paragraph>
|
||||
|
||||
62
src/components/RadioGroup.tsx
Normal file
62
src/components/RadioGroup.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as RadixRadioGroup from "@radix-ui/react-radio-group";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const RadioGroup = ({ value, onChange, children }: Props) => {
|
||||
const [defaultValue] = useState(value);
|
||||
return (
|
||||
<RadixRadioGroup.Root
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className={
|
||||
"flex bg-nb-gray-900 rounded-md border border-nb-gray-700 text-sm items-center justify-center p-1"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</RadixRadioGroup.Root>
|
||||
);
|
||||
};
|
||||
export const RadioGroupItems = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return <div className={"flex w-full bg-nb-gray-900"}>{children}</div>;
|
||||
};
|
||||
|
||||
export const RadioGroupItem = ({
|
||||
value,
|
||||
children,
|
||||
variant = "default",
|
||||
}: {
|
||||
value: string;
|
||||
children?: React.ReactNode;
|
||||
variant?: "default" | "red" | "green";
|
||||
}) => {
|
||||
return (
|
||||
<RadixRadioGroup.Item value={value} asChild={true}>
|
||||
<div
|
||||
key={value}
|
||||
className={cn(
|
||||
variant === "default" &&
|
||||
"text-nb-gray-500 hover:text-nb-gray-400 data-[state=checked]:bg-nb-gray-600 data-[state=checked]:text-nb-gray-100",
|
||||
variant === "red" &&
|
||||
"text-nb-gray-500 hover:text-nb-gray-400 data-[state=checked]:bg-red-800 data-[state=checked]:text-red-200",
|
||||
variant === "green" &&
|
||||
"text-nb-gray-500 hover:text-nb-gray-400 data-[state=checked]:bg-green-800 data-[state=checked]:text-green-200",
|
||||
"cursor-pointer relative transition-all w-full py-1.5 px-5 rounded-md h-full flex items-center text-sm gap-1 text-center justify-center",
|
||||
)}
|
||||
>
|
||||
{children ? children : <div className={"h-3"}></div>}
|
||||
</div>
|
||||
</RadixRadioGroup.Item>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,8 @@ const iconVariant = cva(
|
||||
red: "bg-red-950 border-red-500 text-red-500",
|
||||
gray: "bg-nb-gray-930 border-nb-gray-800 text-gray-500",
|
||||
green: "bg-green-950 border-green-500 text-green-500",
|
||||
purple: "bg-purple-950 border-purple-500 text-purple-500",
|
||||
indigo: "bg-indigo-950 border-indigo-500 text-indigo-500",
|
||||
},
|
||||
size: {
|
||||
small: "w-8 h-8",
|
||||
|
||||
@@ -17,18 +17,18 @@ const Tabs = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>(({ className, onValueChange, ...props }, ref) => {
|
||||
const [value, setValue] = useState(
|
||||
const [tabValue, setTabValue] = useState(
|
||||
props.defaultValue ? props.defaultValue : "",
|
||||
);
|
||||
|
||||
return (
|
||||
<TabContext.Provider value={value}>
|
||||
<TabContext.Provider value={props.value ? props.value : tabValue}>
|
||||
<TabsPrimitive.Root
|
||||
ref={ref}
|
||||
value={value}
|
||||
onValueChange={(value) => {
|
||||
setValue(value);
|
||||
onValueChange && onValueChange(value);
|
||||
value={props.value ? props.value : tabValue}
|
||||
onValueChange={(v) => {
|
||||
setTabValue(v);
|
||||
onValueChange && onValueChange(v);
|
||||
}}
|
||||
className={cn("relative min-w-0", className)}
|
||||
{...props}
|
||||
|
||||
@@ -18,6 +18,10 @@ const switchVariants = cva("", {
|
||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
|
||||
],
|
||||
"red-green": [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
],
|
||||
red: [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
|
||||
@@ -13,7 +13,7 @@ const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className = "px-5 py-3", sideOffset = 7, ...props }, ref) => (
|
||||
>(({ className = "px-4 py-2.5", sideOffset = 7, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
|
||||
@@ -2,17 +2,26 @@ import Button from "@components/Button";
|
||||
import { CommandItem } from "@components/Command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { SelectDropdownSearchInput } from "@components/select/SelectDropdownSearchInput";
|
||||
import { useDebounce } from "@hooks/useDebounce";
|
||||
import useIsVisible from "@hooks/useIsVisible";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Command, CommandGroup, CommandList } from "cmdk";
|
||||
import { trim } from "lodash";
|
||||
import { isEmpty } from "lodash";
|
||||
import { ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ size: number; width: number }>;
|
||||
icon?: React.ComponentType<{
|
||||
size?: number;
|
||||
width?: number;
|
||||
country?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SelectDropdownProps {
|
||||
@@ -21,6 +30,10 @@ interface SelectDropdownProps {
|
||||
disabled?: boolean;
|
||||
popoverWidth?: "auto" | number;
|
||||
options: SelectOption[];
|
||||
showSearch?: boolean;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SelectDropdown({
|
||||
@@ -29,6 +42,10 @@ export function SelectDropdown({
|
||||
disabled = false,
|
||||
popoverWidth = "auto",
|
||||
options,
|
||||
showSearch = false,
|
||||
placeholder = "Select...",
|
||||
searchPlaceholder = "Search...",
|
||||
isLoading = false,
|
||||
}: SelectDropdownProps) {
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
|
||||
@@ -38,29 +55,67 @@ export function SelectDropdown({
|
||||
} else {
|
||||
onChange && onChange(selectedValue);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setSearch("");
|
||||
}, 100);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [slice, setSlice] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
setSlice(options.length);
|
||||
}, 100);
|
||||
} else {
|
||||
setSlice(10);
|
||||
}
|
||||
}, [open, options]);
|
||||
|
||||
const selected = options.find((o) => o.value === value);
|
||||
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(search, 200);
|
||||
|
||||
const filteredItems = React.useMemo(() => {
|
||||
if (isEmpty(debouncedSearch)) return options;
|
||||
return options.filter((item) => {
|
||||
const value = `${item.label}${item.value}` || "";
|
||||
return value.toLowerCase().includes(debouncedSearch.toLowerCase());
|
||||
});
|
||||
}, [options, debouncedSearch]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setSlice(10);
|
||||
if (!isOpen) {
|
||||
setTimeout(() => {
|
||||
setSearch("");
|
||||
}, 100);
|
||||
}
|
||||
setOpen(isOpen);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<PopoverTrigger asChild={true} disabled={disabled || isLoading}>
|
||||
<Button
|
||||
variant={"input"}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isLoading}
|
||||
ref={inputRef}
|
||||
className={"w-full"}
|
||||
>
|
||||
<div className={"w-full flex justify-between items-center gap-2"}>
|
||||
{selected && (
|
||||
{isLoading ? (
|
||||
<div className={"flex gap-2"}>
|
||||
<Skeleton width={20} />
|
||||
<Skeleton width={100} />
|
||||
</div>
|
||||
) : selected ? (
|
||||
<React.Fragment>
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
{selected?.icon && <selected.icon size={14} width={14} />}
|
||||
@@ -71,6 +126,14 @@ export function SelectDropdown({
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
<div className={"flex flex-col text-sm font-medium"}>
|
||||
<span className={"text-nb-gray-200"}>{placeholder}</span>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<div className={"pl-2"}>
|
||||
@@ -91,41 +154,40 @@ export function SelectDropdown({
|
||||
<Command
|
||||
className={"w-full flex"}
|
||||
loop
|
||||
filter={(value, search) => {
|
||||
const formatValue = trim(value.toLowerCase());
|
||||
const formatSearch = trim(search.toLowerCase());
|
||||
if (formatValue.includes(formatSearch)) return 1;
|
||||
return 0;
|
||||
}}
|
||||
filter={() => 0}
|
||||
shouldFilter={false}
|
||||
>
|
||||
<CommandList className={"w-full"}>
|
||||
{showSearch && (
|
||||
<SelectDropdownSearchInput
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
ref={searchRef}
|
||||
placeholder={searchPlaceholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredItems.length == 0 && (
|
||||
<div className={"text-center pb-2 px-3 text-nb-gray-400 text-xs"}>
|
||||
There are no results matching your search.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea
|
||||
className={
|
||||
"max-h-[380px] overflow-y-auto flex flex-col gap-1 pl-2 py-2 pr-3"
|
||||
}
|
||||
className={cn(
|
||||
"max-h-[380px] overflow-y-auto flex flex-col gap-1 pl-2 pb-2 pr-3",
|
||||
!showSearch && "pt-2",
|
||||
)}
|
||||
>
|
||||
<CommandGroup>
|
||||
<div className={"grid grid-cols-1 gap-1"}>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={"py-1 px-2"}
|
||||
onSelect={() => toggle(option.value)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 p-1"}>
|
||||
{option.icon && <option.icon size={14} width={14} />}
|
||||
<div className={"flex flex-col text-sm font-medium"}>
|
||||
<span className={"text-nb-gray-200"}>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{filteredItems.map((option) => (
|
||||
<SelectDropdownItem
|
||||
option={option}
|
||||
toggle={toggle}
|
||||
key={option.value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
@@ -135,3 +197,46 @@ export function SelectDropdown({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const SelectDropdownItem = ({
|
||||
option,
|
||||
toggle,
|
||||
}: {
|
||||
option: SelectOption;
|
||||
toggle: (value: string) => void;
|
||||
}) => {
|
||||
const value = option.value || "" + option.label || "";
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const isVisible = useIsVisible(elementRef);
|
||||
|
||||
const [visible, setVisible] = useState(isVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && !visible) {
|
||||
setVisible(true);
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={"transition-all"}>
|
||||
{visible ? (
|
||||
<CommandItem
|
||||
value={value}
|
||||
ref={elementRef}
|
||||
className={"py-1 px-2"}
|
||||
onSelect={() => toggle(option.value)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 p-1"}>
|
||||
{option.icon && <option.icon size={14} width={14} />}
|
||||
<div className={"flex flex-col text-sm font-medium"}>
|
||||
<span className={"text-nb-gray-200"}>{option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
) : (
|
||||
<div className={"h-[35px] py-1 px-2"}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
55
src/components/select/SelectDropdownSearchInput.tsx
Normal file
55
src/components/select/SelectDropdownSearchInput.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Dispatch, forwardRef } from "react";
|
||||
|
||||
type Props = {
|
||||
search: string;
|
||||
setSearch: Dispatch<React.SetStateAction<string>>;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const SelectDropdownSearchInput = forwardRef<HTMLInputElement, Props>(
|
||||
(
|
||||
{
|
||||
search,
|
||||
setSearch,
|
||||
placeholder = "Search for peers by name or ip...",
|
||||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div className={"relative"}>
|
||||
<input
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
|
||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
|
||||
)}
|
||||
ref={ref}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className={"absolute left-0 top-0 h-full flex items-center pl-4"}>
|
||||
<div className={"flex items-center"}>
|
||||
<SearchIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={"absolute right-0 top-0 h-full flex items-center pr-4"}>
|
||||
<div
|
||||
className={
|
||||
"flex items-center bg-nb-gray-800 py-1 px-1.5 rounded-[4px] border border-nb-gray-500"
|
||||
}
|
||||
>
|
||||
<IconArrowBack size={10} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SelectDropdownSearchInput.displayName = "SelectDropdownSearchInput";
|
||||
@@ -120,6 +120,7 @@ interface DataTableProps<TData, TValue> {
|
||||
showSearch?: boolean;
|
||||
rightSide?: (table: TanStackTable<TData>) => React.ReactNode;
|
||||
manualPagination?: boolean;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
|
||||
@@ -152,6 +153,7 @@ export function DataTableContent<TData, TValue>({
|
||||
searchClassName,
|
||||
rightSide,
|
||||
manualPagination = false,
|
||||
showHeader = true,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const path = usePathname();
|
||||
const [columnFilters, setColumnFilters] = useLocalStorage<ColumnFiltersState>(
|
||||
@@ -250,7 +252,7 @@ export function DataTableContent<TData, TValue>({
|
||||
className={cn("relative mt-8", tableClassName)}
|
||||
minimal={minimal}
|
||||
>
|
||||
{as == "table" && (
|
||||
{showHeader && as == "table" && (
|
||||
<TableHeaderComponent minimal={minimal}>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRowComponent key={headerGroup.id} minimal={minimal}>
|
||||
@@ -361,6 +363,7 @@ export function DataTableContent<TData, TValue>({
|
||||
minimal={minimal}
|
||||
className={cn(
|
||||
onRowClick && "cursor-pointer relative",
|
||||
rowClassName,
|
||||
)}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
|
||||
@@ -9,12 +9,13 @@ const Table = React.forwardRef<
|
||||
React.HTMLAttributes<HTMLTableElement> & TableProps
|
||||
>(({ className, minimal, ...props }, ref) => {
|
||||
return (
|
||||
<div className={cn("relative overflow-x-auto w-full", className)}>
|
||||
<div className={cn("relative overflow-x-auto w-full")}>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"caption-bottom text-sm min-w-full max-w-full w-full",
|
||||
minimal ? "" : "border dark:border-zinc-700/40 border-l-0 border-r-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
56
src/components/ui/CitySelector.tsx
Normal file
56
src/components/ui/CitySelector.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { MapPin } from "lucide-react";
|
||||
import { createElement, useMemo } from "react";
|
||||
import { City } from "@/interfaces/City";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
country: string;
|
||||
};
|
||||
export const CitySelector = ({ value, onChange, country = "de" }: Props) => {
|
||||
const { data: cities, isLoading } = useFetchApi<City[]>(
|
||||
`/locations/countries/${country}/cities`,
|
||||
);
|
||||
|
||||
const cityList = useMemo(() => {
|
||||
const pinIcon = (props: {
|
||||
size?: number;
|
||||
width?: number;
|
||||
country?: string;
|
||||
}) =>
|
||||
createElement(MapPin, {
|
||||
...props,
|
||||
});
|
||||
if (!cities) return [];
|
||||
|
||||
const all = cities?.map((city) => {
|
||||
return {
|
||||
label: city.city_name,
|
||||
value: city.city_name,
|
||||
icon: pinIcon,
|
||||
} as SelectOption;
|
||||
}) as SelectOption[];
|
||||
|
||||
all.unshift({ label: "All Locations", value: "", icon: pinIcon });
|
||||
return all;
|
||||
}, [cities]);
|
||||
|
||||
return (
|
||||
<div className={"block w-full"}>
|
||||
<SelectDropdown
|
||||
isLoading={isLoading}
|
||||
showSearch={true}
|
||||
placeholder={"Select city (optional)..."}
|
||||
searchPlaceholder={"Search city..."}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={cityList || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
52
src/components/ui/CountrySelector.tsx
Normal file
52
src/components/ui/CountrySelector.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { createElement, useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { Country } from "@/interfaces/Country";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
export const CountrySelector = ({ value, onChange }: Props) => {
|
||||
const { data: countries, isLoading } = useFetchApi<Country[]>(
|
||||
"/locations/countries",
|
||||
);
|
||||
|
||||
const countryList = useMemo(() => {
|
||||
return countries?.map((country) => {
|
||||
const flag = (props: {
|
||||
size?: number;
|
||||
width?: number;
|
||||
country?: string;
|
||||
}) =>
|
||||
createElement(RoundedFlag, {
|
||||
country: country.country_code,
|
||||
size: 20,
|
||||
...props,
|
||||
});
|
||||
return {
|
||||
label: country.country_name + " (" + country.country_code + ")",
|
||||
value: country.country_code,
|
||||
icon: flag,
|
||||
} as SelectOption;
|
||||
}) as SelectOption[];
|
||||
}, [countries]);
|
||||
|
||||
return (
|
||||
<div className={"block w-full"}>
|
||||
<SelectDropdown
|
||||
isLoading={isLoading}
|
||||
showSearch={true}
|
||||
placeholder={"Select country..."}
|
||||
searchPlaceholder={"Search country..."}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={countryList || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,13 +5,18 @@ import React from "react";
|
||||
type Props = {
|
||||
text?: string;
|
||||
className?: string;
|
||||
maxChars?: number;
|
||||
};
|
||||
|
||||
export default function DescriptionWithTooltip({ text, className }: Props) {
|
||||
export default function DescriptionWithTooltip({
|
||||
text,
|
||||
className,
|
||||
maxChars = 30,
|
||||
}: Props) {
|
||||
return (
|
||||
<TextWithTooltip
|
||||
text={text}
|
||||
maxChars={30}
|
||||
maxChars={maxChars}
|
||||
className={cn("text-sm text-nb-gray-400 whitespace-nowrap", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
154
src/components/ui/SlidingTabs.tsx
Normal file
154
src/components/ui/SlidingTabs.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const SlidingTabContext = React.createContext(
|
||||
{} as {
|
||||
current: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
back: () => void;
|
||||
},
|
||||
);
|
||||
|
||||
export const useSlidingTabContext = () => {
|
||||
const context = React.useContext(SlidingTabContext);
|
||||
if (!context) {
|
||||
throw new Error("TabContext is not found");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const SlidingTabs = (props: Props) => {
|
||||
const [current, setCurrent] = useState<string | undefined>();
|
||||
|
||||
return (
|
||||
<SlidingTabContext.Provider
|
||||
value={{
|
||||
current: current,
|
||||
onChange: setCurrent,
|
||||
back: () => setCurrent(undefined),
|
||||
}}
|
||||
>
|
||||
<div className={cn("overflow-hidden relative", props.className)}>
|
||||
<AnimatePresence initial={false} mode={"popLayout"}>
|
||||
<motion.div
|
||||
key={current}
|
||||
className={"z-10 relative"}
|
||||
initial={{
|
||||
x: current != undefined ? 50 : -100,
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
x: current != undefined ? 100 : 0,
|
||||
opacity: 0,
|
||||
}}
|
||||
transition={{
|
||||
x: { type: "spring", stiffness: 300, damping: 30, duration: 0.3 },
|
||||
opacity: { duration: 0.15 },
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</SlidingTabContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SlidingTabsList = (props: Props) => {
|
||||
const { onChange, current } = useSlidingTabContext();
|
||||
return !current ? <div>{props.children}</div> : null;
|
||||
};
|
||||
|
||||
export const SlidingTabsTrigger = ({
|
||||
value,
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconClass = "bg-gradient-to-tr from-netbird-200 to-netbird-100",
|
||||
}: {
|
||||
value: string;
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
iconClass?: string;
|
||||
icon?: React.ReactNode;
|
||||
}) => {
|
||||
const { onChange, current } = useSlidingTabContext();
|
||||
return (
|
||||
<div onClick={() => onChange(value)}>
|
||||
<div
|
||||
className={
|
||||
"hover:bg-nb-gray-920/80 border border-transparent hover:border-nb-gray-900 rounded-md flex flex-col items-center transition-all cursor-pointer"
|
||||
}
|
||||
>
|
||||
<div className={"flex gap-4 items-center w-full px-4 py-3"}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 shrink-0 shadow-xl rounded-md flex items-center justify-center select-none",
|
||||
iconClass,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={"pr-10"}>
|
||||
<div className={"text-sm font-medium flex gap-2 items-center"}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={"text-xs mt-0.5 text-nb-gray-300"}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"ml-auto flex gap-2 items-center "}>
|
||||
<ChevronRight size={22} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SlidingTabsPanel = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { current } = useSlidingTabContext();
|
||||
return current == value ? <>{children}</> : null;
|
||||
};
|
||||
|
||||
export const SlidingTabsBackTrigger = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { back } = useSlidingTabContext();
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
back();
|
||||
}}
|
||||
className={"flex gap-2 items-center select-none cursor-pointer"}
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
Back
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -40,6 +40,10 @@ export default function PoliciesProvider({ children }: Props) {
|
||||
enabled: toUpdate.enabled ?? policy.enabled,
|
||||
query: toUpdate.query ?? policy.query ?? "",
|
||||
rules: toUpdate.rules ?? policy.rules ?? [],
|
||||
source_posture_checks:
|
||||
toUpdate.source_posture_checks ??
|
||||
policy.source_posture_checks ??
|
||||
[],
|
||||
},
|
||||
`/${policy.id}`,
|
||||
)
|
||||
|
||||
25
src/hooks/useIsVisible.ts
Normal file
25
src/hooks/useIsVisible.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function useIsVisible(ref: RefObject<HTMLElement>) {
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const [isOnScreen, setIsOnScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
observerRef.current = new IntersectionObserver(([entry]) =>
|
||||
setIsOnScreen(entry.isIntersecting),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && observerRef.current) {
|
||||
observerRef.current.observe(ref.current);
|
||||
}
|
||||
return () => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return isOnScreen;
|
||||
}
|
||||
@@ -7,20 +7,3 @@ export interface AccessToken {
|
||||
last_used: Date;
|
||||
plain_token?: string;
|
||||
}
|
||||
|
||||
export interface SpecificPAT {
|
||||
name: string;
|
||||
user_id: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PersonalAccessTokenGenerated {
|
||||
plain_token: string;
|
||||
personal_access_token: AccessToken;
|
||||
}
|
||||
|
||||
export interface PersonalAccessTokenCreate {
|
||||
user_id: string;
|
||||
name: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
@@ -9,108 +9,3 @@ export interface ActivityEvent {
|
||||
target_id: string;
|
||||
meta: { [key: string]: string };
|
||||
}
|
||||
|
||||
export const ActivityGroupNames = {
|
||||
user: "User",
|
||||
account: "Account",
|
||||
setupkey: "Setup-Key",
|
||||
rule: "Rule",
|
||||
policy: "Policy",
|
||||
peer: "Peer",
|
||||
group: "Group",
|
||||
route: "Route",
|
||||
dns: "DNS",
|
||||
nameserver: "Nameserver",
|
||||
service: "Service",
|
||||
integration: "Integration",
|
||||
dashboard: "Dashboard",
|
||||
};
|
||||
|
||||
export const ActivityEventCodes = {
|
||||
User: {
|
||||
"user.peer.add": "Peer added",
|
||||
"user.join": "User joined",
|
||||
"user.invite": "User invited",
|
||||
"user.peer.delete": "Peer deleted",
|
||||
"user.group.add": "Group added to user",
|
||||
"user.group.delete": "Group removed from user",
|
||||
"user.role.update": "User role updated",
|
||||
"personal.access.token.create": "Personal access token created",
|
||||
"personal.access.token.delete": "Personal access token deleted",
|
||||
"user.block": "User blocked",
|
||||
"user.unblock": "User unblocked",
|
||||
"user.delete": "User deleted",
|
||||
"user.peer.login": "User logged in peer",
|
||||
},
|
||||
Account: {
|
||||
"account.create": "Account created",
|
||||
"account.setting.peer.login.expiration.update":
|
||||
"Account peer login expiration duration updated",
|
||||
"account.setting.peer.login.expiration.enable":
|
||||
"Account peer login expiration enabled",
|
||||
"account.setting.peer.login.expiration.disable":
|
||||
"Account peer login expiration disabled",
|
||||
},
|
||||
"Setup-Key": {
|
||||
"setupkey.peer.add": "Peer added with setup key",
|
||||
"setupkey.add": "Setup key created",
|
||||
"setupkey.update": "Setup key updated",
|
||||
"setupkey.revoke": "Setup key revoked",
|
||||
"setupkey.overuse": "Setup key overused",
|
||||
"setupkey.group.add": "Group added to setup key",
|
||||
"setupkey.group.delete": "Group removed from user setup key",
|
||||
},
|
||||
Rule: {
|
||||
"rule.add": "Rule added",
|
||||
"rule.update": "Rule updated",
|
||||
"rule.delete": "Rule deleted",
|
||||
},
|
||||
Policy: {
|
||||
"policy.add": "Policy added",
|
||||
"policy.update": "Policy updated",
|
||||
"policy.delete": "Policy deleted",
|
||||
},
|
||||
Peer: {
|
||||
"peer.group.add": "Group added to peer",
|
||||
"peer.group.delete": "Group removed from peer",
|
||||
"peer.ssh.enable": "Peer SSH server enabled",
|
||||
"peer.ssh.disable": "Peer SSH server disabled",
|
||||
"peer.rename": "Peer renamed",
|
||||
"peer.login.expiration.enable": "Peer login expiration enabled",
|
||||
"peer.login.expiration.disable": "Peer login expiration disabled",
|
||||
"peer.login.expire": "Peer login expired",
|
||||
},
|
||||
Group: {
|
||||
"group.add": "Group created",
|
||||
"group.update": "Group updated",
|
||||
"group.delete": "Group deleted",
|
||||
},
|
||||
Route: {
|
||||
"route.add": "Route created",
|
||||
"route.delete": "Route deleted",
|
||||
"route.update": "Route updated",
|
||||
},
|
||||
DNS: {
|
||||
"dns.setting.disabled.management.group.add":
|
||||
"Group added to disabled management DNS setting",
|
||||
"dns.setting.disabled.management.group.delete":
|
||||
"Group removed from disabled management DNS setting",
|
||||
},
|
||||
Nameserver: {
|
||||
"nameserver.group.add": "Nameserver group created",
|
||||
"nameserver.group.delete": "Nameserver group deleted",
|
||||
"nameserver.group.update": "Nameserver group updated",
|
||||
},
|
||||
Service: {
|
||||
"service.user.create": "Service user created",
|
||||
"service.user.delete": "Service user deleted",
|
||||
},
|
||||
Integration: {
|
||||
"integration.create": "Integration created",
|
||||
"integration.update": "Integration updated",
|
||||
"integration.delete": "Integration deleted",
|
||||
},
|
||||
Dashboard: {
|
||||
"dashboard.login": "Dashboard login",
|
||||
},
|
||||
};
|
||||
|
||||
4
src/interfaces/City.ts
Normal file
4
src/interfaces/City.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface City {
|
||||
city_name: string;
|
||||
geoname_id: number;
|
||||
}
|
||||
4
src/interfaces/Country.ts
Normal file
4
src/interfaces/Country.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Country {
|
||||
country_code: string;
|
||||
country_name: string;
|
||||
}
|
||||
@@ -21,37 +21,3 @@ export interface Peer {
|
||||
login_expiration_enabled: boolean;
|
||||
approval_required: boolean;
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
groupsNames: string[];
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export interface PeerToSave extends Peer {
|
||||
groupsToSave: string[];
|
||||
}
|
||||
|
||||
export interface PeerGroupsToSave {
|
||||
ID: string;
|
||||
groupsToRemove: string[];
|
||||
groupsToAdd: string[];
|
||||
groupsNoId: string[];
|
||||
}
|
||||
|
||||
export interface PeerNameToIP {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
groupsCount: number;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Policy {
|
||||
enabled: boolean;
|
||||
query: string;
|
||||
rules: PolicyRule[];
|
||||
source_posture_checks: string[];
|
||||
}
|
||||
|
||||
export interface PolicyRule {
|
||||
@@ -23,9 +24,3 @@ export interface PolicyRule {
|
||||
}
|
||||
|
||||
export type Protocol = "all" | "tcp" | "udp" | "icmp";
|
||||
|
||||
export interface PolicyToSave extends Policy {
|
||||
sourcesNoId?: string[];
|
||||
destinationsNoId?: string[];
|
||||
groupsToSave?: string[];
|
||||
}
|
||||
|
||||
125
src/interfaces/PostureCheck.ts
Normal file
125
src/interfaces/PostureCheck.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { SelectOption } from "@components/select/SelectDropdown";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
|
||||
export interface PostureCheck {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
checks: {
|
||||
nb_version_check?: NetBirdVersionCheck;
|
||||
os_version_check?: OperatingSystemVersionCheck;
|
||||
geo_location_check?: GeoLocationCheck;
|
||||
};
|
||||
policies?: Policy[];
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface NetBirdVersionCheck {
|
||||
min_version: string;
|
||||
}
|
||||
|
||||
export interface OperatingSystemVersionCheck {
|
||||
android?: {
|
||||
min_version: string;
|
||||
};
|
||||
darwin?: {
|
||||
min_version: string;
|
||||
};
|
||||
ios?: {
|
||||
min_version: string;
|
||||
};
|
||||
linux?: {
|
||||
min_kernel_version: string;
|
||||
};
|
||||
windows?: {
|
||||
min_kernel_version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GeoLocationCheck {
|
||||
locations: GeoLocation[];
|
||||
action: "allow" | "deny";
|
||||
}
|
||||
|
||||
export interface GeoLocation {
|
||||
id: string;
|
||||
country_code: string;
|
||||
city_name: string;
|
||||
}
|
||||
|
||||
export const windowsKernelVersions: SelectOption[] = [
|
||||
{ value: "5.0", label: "Windows 2000" },
|
||||
{ value: "5.1", label: "Windows XP" },
|
||||
{ value: "6.0", label: "Windows Vista" },
|
||||
{ value: "6.1", label: "Windows 7" },
|
||||
{ value: "6.2", label: "Windows 8" },
|
||||
{ value: "6.3", label: "Windows 8.1" },
|
||||
{ value: "10.0", label: "Windows 10" },
|
||||
{ value: "10.0.2", label: "Windows 11" },
|
||||
];
|
||||
|
||||
export const iOSVersions: SelectOption[] = [
|
||||
{ value: "1.0", label: "iPhone OS 1.x" },
|
||||
{ value: "2.0", label: "iPhone OS 2.x" },
|
||||
{ value: "3.0", label: "iPhone OS 3.x" },
|
||||
{ value: "4.0", label: "iOS 4.x" },
|
||||
{ value: "5.0", label: "iOS 5.x" },
|
||||
{ value: "6.0", label: "iOS 6.x" },
|
||||
{ value: "7.0", label: "iOS 7.x" },
|
||||
{ value: "8.0", label: "iOS 8.x" },
|
||||
{ value: "9.0", label: "iOS 9.x" },
|
||||
{ value: "10.0", label: "iOS 10.x" },
|
||||
{ value: "11.0", label: "iOS 11.x" },
|
||||
{ value: "12.0", label: "iOS 12.x" },
|
||||
{ value: "13.0", label: "iOS 13.x" },
|
||||
{ value: "14.0", label: "iOS 14.x" },
|
||||
{ value: "15.0", label: "iOS 15.x" },
|
||||
{ value: "16.0", label: "iOS 16.x" },
|
||||
{ value: "17.0", label: "iOS 17.x" },
|
||||
];
|
||||
|
||||
export const macOSVersions: SelectOption[] = [
|
||||
{ value: "10.0", label: "Mac OS X Cheetah" },
|
||||
{ value: "10.1", label: "Mac OS X Puma" },
|
||||
{ value: "10.2", label: "Mac OS X Jaguar" },
|
||||
{ value: "10.3", label: "Mac OS X Panther" },
|
||||
{ value: "10.4", label: "Mac OS X Tiger" },
|
||||
{ value: "10.5", label: "Mac OS X Leopard" },
|
||||
{ value: "10.6", label: "Mac OS X Snow Leopard" },
|
||||
{ value: "10.7", label: "Mac OS X Lion" },
|
||||
{ value: "10.8", label: "OS X Mountain Lion" },
|
||||
{ value: "10.9", label: "OS X Mavericks" },
|
||||
{ value: "10.10", label: "OS X Yosemite" },
|
||||
{ value: "10.11", label: "OS X El Capitan" },
|
||||
{ value: "10.12", label: "macOS Sierra" },
|
||||
{ value: "10.13", label: "macOS High Sierra" },
|
||||
{ value: "10.14", label: "macOS Mojave" },
|
||||
{ value: "10.15", label: "macOS Catalina" },
|
||||
{ value: "11.0", label: "macOS Big Sur" },
|
||||
{ value: "12.0", label: "macOS Monterey" },
|
||||
{ value: "13.0", label: "macOS Ventura" },
|
||||
{ value: "14.0", label: "macOS Sonoma" },
|
||||
];
|
||||
|
||||
export const androidVersions: SelectOption[] = [
|
||||
{ value: "1.5", label: "Android Cupcake" },
|
||||
{ value: "1.6", label: "Android Donut" },
|
||||
{ value: "2.0", label: "Android Eclair" },
|
||||
{ value: "2.2", label: "Android Froyo" },
|
||||
{ value: "2.3", label: "Android Gingerbread" },
|
||||
{ value: "3.0", label: "Android Honeycomb" },
|
||||
{ value: "4.0", label: "Android Ice Cream Sandwich" },
|
||||
{ value: "4.1", label: "Android Jelly Bean" },
|
||||
{ value: "4.4", label: "Android KitKat" },
|
||||
{ value: "5.0", label: "Android Lollipop" },
|
||||
{ value: "6.0", label: "Android Marshmallow" },
|
||||
{ value: "7.0", label: "Android Nougat" },
|
||||
{ value: "8.0", label: "Android Oreo" },
|
||||
{ value: "9.0", label: "Android Pie" },
|
||||
{ value: "10", label: "Android 10" },
|
||||
{ value: "11", label: "Android 11" },
|
||||
{ value: "12", label: "Android 12" },
|
||||
{ value: "13", label: "Android 13" },
|
||||
{ value: "14", label: "Android 14" },
|
||||
{ value: "15", label: "Android 15" },
|
||||
];
|
||||
@@ -11,15 +11,6 @@ export interface User {
|
||||
last_login?: Date;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
autoGroupsNames: string[];
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface UserToSave extends User {
|
||||
groupsToCreate: string[];
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
User = "user",
|
||||
Admin = "admin",
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export interface Version {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
}
|
||||
|
||||
export interface NetbirdRelease {
|
||||
latest_version: string;
|
||||
last_checked: Date;
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function Navigation({
|
||||
hideOnMobile ? "hidden md:block" : "",
|
||||
fullWidth
|
||||
? "w-auto max-w-[22rem]"
|
||||
: "w-[14rem] min-w-[14rem] overflow-y-auto",
|
||||
: "w-[15rem] min-w-[15rem] overflow-y-auto",
|
||||
)}
|
||||
theme={customTheme}
|
||||
style={{
|
||||
@@ -69,8 +69,22 @@ export default function Navigation({
|
||||
<SidebarItem
|
||||
icon={<AccessControlIcon />}
|
||||
label="Access Control"
|
||||
href={"/access-control"}
|
||||
/>
|
||||
collapsible
|
||||
>
|
||||
<SidebarItem
|
||||
label="Policies"
|
||||
href={"/access-control"}
|
||||
isChild
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
<SidebarItem
|
||||
label="Posture Checks"
|
||||
isChild
|
||||
href={"/posture-checks"}
|
||||
exactPathMatch={true}
|
||||
/>
|
||||
</SidebarItem>
|
||||
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
label="Network Routes"
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import PolicyDirection, { Direction } from "@components/ui/PolicyDirection";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { uniqBy } from "lodash";
|
||||
import {
|
||||
@@ -42,13 +42,16 @@ import {
|
||||
Shield,
|
||||
Text,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Policy, Protocol } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { PostureCheckTab } from "@/modules/posture-checks/ui/PostureCheckTab";
|
||||
import { PostureCheckTabTrigger } from "@/modules/posture-checks/ui/PostureCheckTabTrigger";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@@ -108,11 +111,15 @@ export function AccessControlModalContent({
|
||||
policy,
|
||||
cell,
|
||||
}: ModalProps) {
|
||||
const { data: allPostureChecks, isLoading: isPostureChecksLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
const { updatePolicy } = usePolicies();
|
||||
const firstRule = policy?.rules ? policy.rules[0] : undefined;
|
||||
|
||||
const [tab, setTab] = useState(() => {
|
||||
if (!cell) return "policy";
|
||||
if (cell == "posture_checks") return "posture_checks";
|
||||
if (cell == "name") return "general";
|
||||
return "policy";
|
||||
});
|
||||
@@ -189,6 +196,9 @@ export function AccessControlModalContent({
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
source_posture_checks: postureChecks
|
||||
? postureChecks.map((c) => c.id)
|
||||
: undefined,
|
||||
rules: [
|
||||
{
|
||||
bidirectional: direction == "bi",
|
||||
@@ -235,6 +245,29 @@ export function AccessControlModalContent({
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports, name]);
|
||||
|
||||
const [postureChecks, setPostureChecks] = useState<PostureCheck[]>([]);
|
||||
const postureChecksLoaded = useRef(false);
|
||||
|
||||
const initialPostureChecks = useMemo(() => {
|
||||
return (
|
||||
allPostureChecks?.filter((check) => {
|
||||
if (policy?.source_posture_checks) {
|
||||
return policy.source_posture_checks.includes(check.id);
|
||||
}
|
||||
return false;
|
||||
}) || []
|
||||
);
|
||||
}, [policy, allPostureChecks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (postureChecksLoaded.current) return;
|
||||
|
||||
if (initialPostureChecks.length > 0) {
|
||||
postureChecksLoaded.current = true;
|
||||
setPostureChecks(initialPostureChecks);
|
||||
}
|
||||
}, [initialPostureChecks]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-2xl"}>
|
||||
<ModalHeader
|
||||
@@ -256,6 +289,7 @@ export function AccessControlModalContent({
|
||||
<ArrowRightLeft size={16} />
|
||||
Policy
|
||||
</TabsTrigger>
|
||||
<PostureCheckTabTrigger />
|
||||
<TabsTrigger value={"general"}>
|
||||
<Text
|
||||
size={16}
|
||||
@@ -368,6 +402,11 @@ export function AccessControlModalContent({
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<PostureCheckTab
|
||||
isLoading={isPostureChecksLoading}
|
||||
postureChecks={postureChecks}
|
||||
setPostureChecks={setPostureChecks}
|
||||
/>
|
||||
<TabsContent value={"general"} className={"px-8 pb-6"}>
|
||||
<div className={"flex flex-col gap-6"}>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Badge from "@components/Badge";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
|
||||
type Props = {
|
||||
policy: Policy;
|
||||
};
|
||||
export default function AccessControlPostureCheckCell({ policy }: Props) {
|
||||
return policy.source_posture_checks &&
|
||||
policy.source_posture_checks.length > 0 ? (
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<ShieldCheck size={14} className={"text-green-500"} />
|
||||
{policy.source_posture_checks.length} Posture Check(s)
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<IconCirclePlus size={14} />
|
||||
Add Posture Check
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import AccessControlDestinationsCell from "@/modules/access-control/table/Access
|
||||
import AccessControlDirectionCell from "@/modules/access-control/table/AccessControlDirectionCell";
|
||||
import AccessControlNameCell from "@/modules/access-control/table/AccessControlNameCell";
|
||||
import AccessControlPortsCell from "@/modules/access-control/table/AccessControlPortsCell";
|
||||
import AccessControlPostureCheckCell from "@/modules/access-control/table/AccessControlPostureCheckCell";
|
||||
import AccessControlProtocolCell from "@/modules/access-control/table/AccessControlProtocolCell";
|
||||
import AccessControlSourcesCell from "@/modules/access-control/table/AccessControlSourcesCell";
|
||||
|
||||
@@ -142,6 +143,17 @@ export const AccessControlTableColumns: ColumnDef<Policy>[] = [
|
||||
},
|
||||
cell: ({ cell }) => <AccessControlPortsCell policy={cell.row.original} />,
|
||||
},
|
||||
{
|
||||
id: "posture_checks",
|
||||
accessorFn: (row) => row.source_posture_checks?.length || 0,
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Posture Checks</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlPostureCheckCell policy={cell.row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@components/Tooltip";
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
import { FcAndroidOs, FcLinux } from "react-icons/fc";
|
||||
import IOSIcon from "@/assets/icons/IOSIcon";
|
||||
import AppleLogo from "@/assets/os-icons/apple.svg";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
@@ -39,7 +40,7 @@ export function PeerOSCell({ os }: { os: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function OSLogo({ os }: { os: string }) {
|
||||
export function OSLogo({ os }: { os: string }) {
|
||||
const icon = useMemo(() => {
|
||||
return getOperatingSystem(os.toLowerCase());
|
||||
}, [os]);
|
||||
@@ -48,6 +49,8 @@ function OSLogo({ os }: { os: string }) {
|
||||
return <FaWindows className={"text-white text-lg"} />;
|
||||
if (icon === OperatingSystem.APPLE)
|
||||
return <Image src={AppleLogo} alt={""} width={14} />;
|
||||
if (icon === OperatingSystem.IOS)
|
||||
return <IOSIcon className={"fill-white"} size={20} />;
|
||||
if (icon === OperatingSystem.ANDROID)
|
||||
return <FcAndroidOs className={"text-white text-2xl brightness-200"} />;
|
||||
|
||||
|
||||
203
src/modules/posture-checks/checks/PostureCheckGeoLocation.tsx
Normal file
203
src/modules/posture-checks/checks/PostureCheckGeoLocation.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Label } from "@components/Label";
|
||||
import { ModalClose, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
|
||||
import { CitySelector } from "@components/ui/CitySelector";
|
||||
import { CountrySelector } from "@components/ui/CountrySelector";
|
||||
import { isEmpty, uniqueId } from "lodash";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
FlagIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircle,
|
||||
ShieldCheck,
|
||||
ShieldXIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { GeoLocation, GeoLocationCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: GeoLocationCheck;
|
||||
onChange: (value: GeoLocationCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckGeoLocation = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon={<FlagIcon size={16} />}
|
||||
title={"Country & Region"}
|
||||
description={
|
||||
"Restrict access in your network based on country or region."
|
||||
}
|
||||
iconClass={"bg-gradient-to-tr from-indigo-500 to-indigo-400"}
|
||||
modalWidthClass={"max-w-2xl"}
|
||||
active={value ? value?.locations?.length > 0 : false}
|
||||
onReset={() => onChange(undefined)}
|
||||
license={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
This check includes GeoLite2 data created by MaxMind, available from{" "}
|
||||
<InlineLink href={"https://www.maxmind.com"} target={"_blank"}>
|
||||
https://www.maxmind.com
|
||||
</InlineLink>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [allowDenyLocation, setAllowDenyLocation] = useState<string>(
|
||||
value?.action ? value.action : "allow",
|
||||
);
|
||||
const [locations, setLocations] = useState<GeoLocation[]>(
|
||||
value?.locations.map((l) => {
|
||||
return {
|
||||
id: uniqueId("location"),
|
||||
country_code: l.country_code,
|
||||
city_name: l.city_name || "",
|
||||
};
|
||||
}) || [],
|
||||
);
|
||||
|
||||
const updateLocation = (id: string, location: GeoLocation) => {
|
||||
const find = locations.find((l) => l.id === id);
|
||||
if (find) {
|
||||
Object.assign(find, location);
|
||||
setLocations([...locations]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeLocation = (id: string) => {
|
||||
setLocations(locations.filter((l) => l.id !== id));
|
||||
};
|
||||
|
||||
const addLocation = () => {
|
||||
setLocations([
|
||||
...locations,
|
||||
{ id: uniqueId("location"), country_code: "AF", city_name: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex flex-col px-8 gap-2 pb-6"}>
|
||||
<div className={"flex justify-between items-start gap-10 mt-2"}>
|
||||
<div>
|
||||
<Label>Allow or Block Location</Label>
|
||||
<HelpText className={""}>
|
||||
Choose whether you want to allow or block access from specific
|
||||
countries or regions
|
||||
</HelpText>
|
||||
</div>
|
||||
<RadioGroup value={allowDenyLocation} onChange={setAllowDenyLocation}>
|
||||
<RadioGroupItem value={"allow"} variant={"green"}>
|
||||
<ShieldCheck size={16} />
|
||||
Allow
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value={"deny"} variant={"red"}>
|
||||
<ShieldXIcon size={16} />
|
||||
Block
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{locations.length > 0 && (
|
||||
<div className={"mb-2 flex flex-col gap-2 w-full "}>
|
||||
{locations.map((location) => {
|
||||
return (
|
||||
<div key={location.id} className={"flex gap-2"}>
|
||||
<CountrySelector
|
||||
value={location.country_code}
|
||||
onChange={(value) => {
|
||||
updateLocation(location.id, {
|
||||
...location,
|
||||
country_code: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{location.country_code && (
|
||||
<CitySelector
|
||||
value={location.city_name || ""}
|
||||
onChange={(value) => {
|
||||
updateLocation(location.id, {
|
||||
...location,
|
||||
city_name: value,
|
||||
});
|
||||
}}
|
||||
country={location.country_code}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={() => removeLocation(location.id)}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant={"dotted"}
|
||||
size={"sm"}
|
||||
disabled={allowDenyLocation == "all"}
|
||||
onClick={addLocation}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Location
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink href={"#"} target={"_blank"}>
|
||||
Country & Region Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => {
|
||||
if (isEmpty(locations)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange({
|
||||
action: allowDenyLocation as "allow" | "deny",
|
||||
locations: locations,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
116
src/modules/posture-checks/checks/PostureCheckNetBirdVersion.tsx
Normal file
116
src/modules/posture-checks/checks/PostureCheckNetBirdVersion.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { ModalClose, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { isEmpty } from "lodash";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { NetBirdVersionCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: NetBirdVersionCheck;
|
||||
onChange: (value: NetBirdVersionCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckNetBirdVersion = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
key={open ? 1 : 0}
|
||||
active={value?.min_version !== undefined}
|
||||
title={"NetBird Client Version"}
|
||||
description={
|
||||
"Restrict access to peers with a specific NetBird client version."
|
||||
}
|
||||
icon={<NetBirdIcon size={18} />}
|
||||
modalWidthClass={"max-w-lg"}
|
||||
onReset={() => onChange(undefined)}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [version, setVersion] = useState(value?.min_version || "");
|
||||
|
||||
const versionError = useMemo(() => {
|
||||
if (version == "") return "";
|
||||
const validSemver = validator.isValidVersion(version);
|
||||
if (!validSemver)
|
||||
return "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
|
||||
}, [version]);
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
return !versionError && version !== value?.min_version && !isEmpty(version);
|
||||
}, [version, versionError, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex flex-col px-8 gap-3 pb-6"}>
|
||||
<div>
|
||||
<Label>Minimum required version</Label>
|
||||
<HelpText>
|
||||
Only peers with the minimum specified NetBird client version will
|
||||
have access to the network.
|
||||
</HelpText>
|
||||
<div>
|
||||
<Input
|
||||
className={"max-w-[200px]"}
|
||||
value={version}
|
||||
onChange={(e) => setVersion(e.target.value)}
|
||||
placeholder={"e.g., 0.25.0"}
|
||||
error={versionError}
|
||||
customPrefix={"Version"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink href={"#"} target={"_blank"}>
|
||||
Client Version Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (isEmpty(version)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange({ min_version: version });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,416 @@
|
||||
import Button from "@components/Button";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { ModalClose, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { IconMathEqualGreater } from "@tabler/icons-react";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
Disc3Icon,
|
||||
ExternalLinkIcon,
|
||||
FileCog,
|
||||
GalleryHorizontalEnd,
|
||||
ShieldCheck,
|
||||
ShieldXIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import AndroidIcon from "@/assets/icons/AndroidIcon";
|
||||
import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
import IOSIcon from "@/assets/icons/IOSIcon";
|
||||
import { LinuxIcon } from "@/assets/icons/LinuxIcon";
|
||||
import WindowsIcon from "@/assets/icons/WindowsIcon";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import {
|
||||
androidVersions,
|
||||
iOSVersions,
|
||||
macOSVersions,
|
||||
OperatingSystemVersionCheck,
|
||||
windowsKernelVersions,
|
||||
} from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: OperatingSystemVersionCheck;
|
||||
onChange: (value: OperatingSystemVersionCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckOperatingSystem = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
key={open ? 1 : 0}
|
||||
icon={<Disc3Icon size={16} />}
|
||||
title={"Operating System"}
|
||||
modalWidthClass={"max-w-xl"}
|
||||
description={
|
||||
"Restrict access in your network based on the operating system."
|
||||
}
|
||||
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
|
||||
active={value !== undefined}
|
||||
onReset={() => onChange(undefined)}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [tab] = useState(String(OperatingSystem.LINUX));
|
||||
|
||||
const firstTimeCheck = value === undefined;
|
||||
|
||||
const [windowsVersion, setWindowsVersion] = useState<string>(
|
||||
firstTimeCheck
|
||||
? ""
|
||||
: value && value.windows
|
||||
? value.windows.min_kernel_version
|
||||
: "-",
|
||||
);
|
||||
const [macOSVersion, setMacOSVersion] = useState<string>(
|
||||
firstTimeCheck
|
||||
? ""
|
||||
: value && value.darwin
|
||||
? value.darwin?.min_version
|
||||
: "-",
|
||||
);
|
||||
const [androidVersion, setAndroidVersion] = useState<string>(
|
||||
firstTimeCheck
|
||||
? ""
|
||||
: value && value.android
|
||||
? value.android?.min_version
|
||||
: "-",
|
||||
);
|
||||
const [iOSVersion, setIOSVersion] = useState<string>(
|
||||
firstTimeCheck ? "" : value && value.ios ? value.ios?.min_version : "-",
|
||||
);
|
||||
const [linuxVersion, setLinuxVersion] = useState<string>(
|
||||
firstTimeCheck
|
||||
? ""
|
||||
: value && value.linux
|
||||
? value.linux?.min_kernel_version
|
||||
: "-",
|
||||
);
|
||||
|
||||
const [linuxError, setLinuxError] = useState("");
|
||||
const [windowsError, setWindowsError] = useState("");
|
||||
const [macOSError, setMacOSError] = useState("");
|
||||
const [iOSError, setIOSError] = useState("");
|
||||
const [androidError, setAndroidError] = useState("");
|
||||
|
||||
const versionError =
|
||||
linuxError || windowsError || macOSError || iOSError || androidError;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={String(OperatingSystem.LINUX)}>
|
||||
<LinuxIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Linux
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
|
||||
<WindowsIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Windows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.APPLE)}>
|
||||
<AppleIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
macOS
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.IOS)}>
|
||||
<IOSIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
iOS
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
|
||||
<AndroidIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Android
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={String(OperatingSystem.LINUX)} className={"px-8"}>
|
||||
<OperatingSystemTab
|
||||
value={linuxVersion}
|
||||
onChange={setLinuxVersion}
|
||||
os={OperatingSystem.LINUX}
|
||||
onError={setLinuxError}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value={String(OperatingSystem.WINDOWS)} className={"px-8"}>
|
||||
<OperatingSystemTab
|
||||
versionList={windowsKernelVersions}
|
||||
value={windowsVersion}
|
||||
onChange={setWindowsVersion}
|
||||
os={OperatingSystem.WINDOWS}
|
||||
onError={setWindowsError}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value={String(OperatingSystem.APPLE)} className={"px-8"}>
|
||||
<OperatingSystemTab
|
||||
versionList={macOSVersions}
|
||||
value={macOSVersion}
|
||||
onChange={setMacOSVersion}
|
||||
os={OperatingSystem.APPLE}
|
||||
onError={setMacOSError}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value={String(OperatingSystem.IOS)} className={"px-8"}>
|
||||
<OperatingSystemTab
|
||||
versionList={iOSVersions}
|
||||
value={iOSVersion}
|
||||
onChange={setIOSVersion}
|
||||
os={OperatingSystem.IOS}
|
||||
onError={setIOSError}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value={String(OperatingSystem.ANDROID)} className={"px-8"}>
|
||||
<OperatingSystemTab
|
||||
versionList={androidVersions}
|
||||
value={androidVersion}
|
||||
onChange={setAndroidVersion}
|
||||
os={OperatingSystem.ANDROID}
|
||||
onError={setAndroidError}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className={"h-6"}></div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink href={"#"} target={"_blank"}>
|
||||
Operating System Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
disabled={!!versionError}
|
||||
variant={"primary"}
|
||||
onClick={() => {
|
||||
const osCheck = {} as OperatingSystemVersionCheck;
|
||||
|
||||
if (windowsVersion !== "-") {
|
||||
osCheck.windows = { min_kernel_version: windowsVersion };
|
||||
}
|
||||
if (macOSVersion !== "-") {
|
||||
osCheck.darwin = { min_version: macOSVersion };
|
||||
}
|
||||
if (androidVersion !== "-") {
|
||||
osCheck.android = { min_version: androidVersion };
|
||||
}
|
||||
if (iOSVersion !== "-") {
|
||||
osCheck.ios = { min_version: iOSVersion };
|
||||
}
|
||||
if (linuxVersion !== "-") {
|
||||
osCheck.linux = { min_kernel_version: linuxVersion };
|
||||
}
|
||||
|
||||
if (isEmpty(osCheck)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange(osCheck);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type OperatingSystemTabProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
versionList?: SelectOption[];
|
||||
os: OperatingSystem;
|
||||
onError: (error: string) => void;
|
||||
};
|
||||
|
||||
const allOrMinOptions = [
|
||||
{
|
||||
label: "All versions",
|
||||
value: "all",
|
||||
icon: GalleryHorizontalEnd,
|
||||
},
|
||||
{
|
||||
label: "Equal or greater than",
|
||||
value: "min",
|
||||
icon: IconMathEqualGreater,
|
||||
},
|
||||
] as SelectOption[];
|
||||
|
||||
export const OperatingSystemTab = ({
|
||||
value,
|
||||
onChange,
|
||||
versionList,
|
||||
os,
|
||||
onError,
|
||||
}: OperatingSystemTabProps) => {
|
||||
const [allow, setAllow] = useState(value == "-" ? "block" : "allow");
|
||||
const [allOrMin, setAllOrMin] = useState(
|
||||
value == "" || value == "-" || value == "0" ? "all" : "min",
|
||||
);
|
||||
const [useCustomVersion, setUseCustomVersion] = useState(() => {
|
||||
if (!versionList) return false;
|
||||
if (!value) return false;
|
||||
if (value === "-") return false;
|
||||
if (value === "0") return false;
|
||||
const find = versionList.map((v) => v.value).includes(value);
|
||||
return !find;
|
||||
});
|
||||
|
||||
const changeAllow = (value: string) => {
|
||||
setAllow(value);
|
||||
if (value === "block") {
|
||||
setAllOrMin("all");
|
||||
onChange("-");
|
||||
setAllOrMin("all");
|
||||
setUseCustomVersion(false);
|
||||
} else {
|
||||
onChange("");
|
||||
setAllOrMin("all");
|
||||
setUseCustomVersion(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changeAllOrMin = (option: string) => {
|
||||
setAllOrMin(option);
|
||||
if (option === "all") {
|
||||
onChange("");
|
||||
} else if (option === "min" && value == "" && versionList) {
|
||||
const getLast = versionList[versionList.length - 1];
|
||||
onChange(getLast.value);
|
||||
}
|
||||
};
|
||||
|
||||
const prefix =
|
||||
os === OperatingSystem.LINUX || os === OperatingSystem.WINDOWS
|
||||
? "Kernel Version"
|
||||
: "Version";
|
||||
|
||||
const versionError = useMemo(() => {
|
||||
const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
|
||||
if (value == "") return "";
|
||||
if (value == "-") return "";
|
||||
const validSemver = validator.isValidVersion(value);
|
||||
if (!validSemver) return msg;
|
||||
return "";
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onError(versionError);
|
||||
}, [versionError]);
|
||||
|
||||
return (
|
||||
<div className={""}>
|
||||
<div className={"flex justify-between items-start gap-10 "}>
|
||||
<div>
|
||||
<Label>Allow or Block</Label>
|
||||
<HelpText>
|
||||
Choose whether you want to allow or block the operating system.
|
||||
</HelpText>
|
||||
</div>
|
||||
<RadioGroup value={allow} onChange={changeAllow}>
|
||||
<RadioGroupItem value={"allow"} variant={"green"}>
|
||||
<ShieldCheck size={14} />
|
||||
Allow
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value={"block"} variant={"red"}>
|
||||
<ShieldXIcon size={14} />
|
||||
Block
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className={"gap-4 items-center grid grid-cols-2 mt-3"}>
|
||||
<SelectDropdown
|
||||
value={allOrMin}
|
||||
onChange={changeAllOrMin}
|
||||
options={allOrMinOptions}
|
||||
disabled={allow === "block"}
|
||||
/>
|
||||
{versionList && !useCustomVersion ? (
|
||||
<SelectDropdown
|
||||
value={value || "0"}
|
||||
showSearch={true}
|
||||
placeholder={"Select version..."}
|
||||
onChange={onChange}
|
||||
options={versionList}
|
||||
disabled={allOrMin === "all" || allow === "block"}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={value}
|
||||
customPrefix={prefix}
|
||||
placeholder={"e.g., 6.0.0"}
|
||||
error={versionError}
|
||||
errorTooltip={true}
|
||||
disabled={allOrMin === "all" || allow === "block"}
|
||||
onChange={(v) => {
|
||||
onChange(v.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{os !== OperatingSystem.LINUX && (
|
||||
<div className={"mt-4"}>
|
||||
<FancyToggleSwitch
|
||||
disabled={allow === "block" || allOrMin === "all"}
|
||||
value={useCustomVersion}
|
||||
onChange={setUseCustomVersion}
|
||||
label={
|
||||
<>
|
||||
<FileCog size={14} />
|
||||
Use custom version number
|
||||
</>
|
||||
}
|
||||
helpText={"Use a custom version number if you need more control."}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import useFetchApi from "@utils/api";
|
||||
import * as React from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { Country } from "@/interfaces/Country";
|
||||
import { GeoLocationCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
check?: GeoLocationCheck;
|
||||
};
|
||||
export const GeoLocationTooltip = ({ children, check }: Props) => {
|
||||
const { data: countries } = useFetchApi<Country[]>(`/locations/countries`);
|
||||
|
||||
return check ? (
|
||||
<FullTooltip
|
||||
className={"w-full"}
|
||||
interactive={true}
|
||||
contentClassName={"p-0"}
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"text-neutral-300 flex flex-col items-start text-sm gap-1 justify-start min-w-[200px]"
|
||||
}
|
||||
>
|
||||
<div className={"px-4 pt-3"}>
|
||||
{check.action == "allow" ? (
|
||||
<span>
|
||||
<span className={"text-green-500 font-semibold"}>
|
||||
Allow only
|
||||
</span>{" "}
|
||||
the following <br />
|
||||
countries & regions
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className={"text-red-500 font-semibold"}>Block</span> the
|
||||
following <br />
|
||||
countries & regions
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
className={"max-h-[285px] overflow-y-auto flex flex-col px-4"}
|
||||
>
|
||||
<div className={"flex flex-col gap-1.5 mt-2 text-xs mb-3 w-full"}>
|
||||
{check.locations.map((location, index) => {
|
||||
const country = countries?.find(
|
||||
(c) => c.country_code === location.country_code,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={" rounded-full flex items-center gap-2 pr-4"}
|
||||
key={index}
|
||||
>
|
||||
<div
|
||||
className={"border-2 border-nb-gray-900/50 rounded-full"}
|
||||
>
|
||||
<RoundedFlag country={location.country_code} size={23} />
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center"}>
|
||||
<span className={"font-semibold"}>
|
||||
{country && country.country_name}
|
||||
</span>
|
||||
|
||||
{location.city_name && `, ${location.city_name}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
check
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { IconMathEqualGreater } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
version?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const NetBirdVersionTooltip = ({ version, children }: Props) => {
|
||||
return version ? (
|
||||
<FullTooltip
|
||||
className={"w-full"}
|
||||
interactive={false}
|
||||
content={
|
||||
<div className={"text-neutral-300 flex items-center text-sm gap-1"}>
|
||||
<span className={""}>Min. Client Version</span>
|
||||
|
||||
<span
|
||||
className={"text-netbird font-semibold flex items-center gap-1"}
|
||||
>
|
||||
<IconMathEqualGreater size={14} />
|
||||
{version}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import * as React from "react";
|
||||
import AndroidIcon from "@/assets/icons/AndroidIcon";
|
||||
import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
import IOSIcon from "@/assets/icons/IOSIcon";
|
||||
import { LinuxIcon } from "@/assets/icons/LinuxIcon";
|
||||
import WindowsIcon from "@/assets/icons/WindowsIcon";
|
||||
import {
|
||||
androidVersions,
|
||||
iOSVersions,
|
||||
macOSVersions,
|
||||
OperatingSystemVersionCheck,
|
||||
windowsKernelVersions,
|
||||
} from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckOperatingSystemInfo } from "@/modules/posture-checks/ui/PostureCheckOperatingSystemInfo";
|
||||
|
||||
type Props = {
|
||||
check?: OperatingSystemVersionCheck;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export const OperatingSystemTooltip = ({ check, children }: Props) => {
|
||||
return check ? (
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
contentClassName={"p-2.5"}
|
||||
className={"w-full"}
|
||||
content={
|
||||
<div>
|
||||
<div className={"flex flex-col gap-1"}>
|
||||
<PostureCheckOperatingSystemInfo
|
||||
icon={LinuxIcon}
|
||||
os={"Linux"}
|
||||
version={check.linux?.min_kernel_version}
|
||||
versionText={"Kernel Version"}
|
||||
/>
|
||||
<PostureCheckOperatingSystemInfo
|
||||
icon={WindowsIcon}
|
||||
os={"Windows"}
|
||||
version={check.windows?.min_kernel_version}
|
||||
versionText={"Kernel Version"}
|
||||
versionList={windowsKernelVersions}
|
||||
/>
|
||||
<PostureCheckOperatingSystemInfo
|
||||
icon={AppleIcon}
|
||||
os={"macOS"}
|
||||
version={check.darwin?.min_version}
|
||||
versionList={macOSVersions}
|
||||
/>
|
||||
<PostureCheckOperatingSystemInfo
|
||||
icon={IOSIcon}
|
||||
os={"iOS"}
|
||||
version={check.ios?.min_version}
|
||||
versionList={iOSVersions}
|
||||
/>
|
||||
<PostureCheckOperatingSystemInfo
|
||||
icon={AndroidIcon}
|
||||
os={"Android"}
|
||||
version={check.android?.min_version}
|
||||
versionList={androidVersions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
33
src/modules/posture-checks/modal/PostureCheckBrowseModal.tsx
Normal file
33
src/modules/posture-checks/modal/PostureCheckBrowseModal.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Modal, ModalContent } from "@components/modal/Modal";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import PostureCheckBrowseTable from "@/modules/posture-checks/table/PostureCheckBrowseTable";
|
||||
|
||||
type Props = {
|
||||
onSuccess: (checks: PostureCheck[]) => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
export const PostureCheckBrowseModal = ({
|
||||
onSuccess,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative", "max-w-2xl")}
|
||||
className={"pb-0"}
|
||||
showClose={false}
|
||||
>
|
||||
<PostureCheckBrowseTable
|
||||
onAdd={(checks) => {
|
||||
onSuccess(checks);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
269
src/modules/posture-checks/modal/PostureCheckModal.tsx
Normal file
269
src/modules/posture-checks/modal/PostureCheckModal.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { notify } from "@components/Notification";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isEmpty } from "lodash";
|
||||
import { ExternalLinkIcon, LayoutList, ShieldCheck, Text } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import {
|
||||
GeoLocationCheck,
|
||||
OperatingSystemVersionCheck,
|
||||
PostureCheck,
|
||||
} from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckGeoLocation } from "@/modules/posture-checks/checks/PostureCheckGeoLocation";
|
||||
import { PostureCheckNetBirdVersion } from "@/modules/posture-checks/checks/PostureCheckNetBirdVersion";
|
||||
import { PostureCheckOperatingSystem } from "@/modules/posture-checks/checks/PostureCheckOperatingSystem";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: (check: PostureCheck) => void;
|
||||
postureCheck?: PostureCheck;
|
||||
};
|
||||
|
||||
export default function PostureCheckModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
postureCheck,
|
||||
}: Props) {
|
||||
const postureCheckRequest = useApiCall("/posture-checks");
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [name, setName] = useState(postureCheck?.name || "");
|
||||
const [description, setDescription] = useState(
|
||||
postureCheck?.description || "",
|
||||
);
|
||||
|
||||
const [nbVersionCheck, setNbVersionCheck] = useState(
|
||||
postureCheck?.checks.nb_version_check || undefined,
|
||||
);
|
||||
const [geoLocationCheck, setGeoLocationCheckCheck] = useState(
|
||||
postureCheck?.checks.geo_location_check || undefined,
|
||||
);
|
||||
const [osVersionCheck, setOsVersionCheck] = useState(
|
||||
postureCheck?.checks.os_version_check || undefined,
|
||||
);
|
||||
|
||||
const validateOSCheck = (osCheck?: OperatingSystemVersionCheck) => {
|
||||
if (!osCheck) return;
|
||||
const os = osCheck;
|
||||
if (os.darwin && os.darwin.min_version == "") os.darwin.min_version = "0";
|
||||
if (os.android && os.android.min_version == "")
|
||||
os.android.min_version = "0";
|
||||
if (os.windows && os.windows.min_kernel_version == "")
|
||||
os.windows.min_kernel_version = "0";
|
||||
if (os.linux && os.linux.min_kernel_version == "")
|
||||
os.linux.min_kernel_version = "0";
|
||||
if (os.ios && os.ios.min_version == "") os.ios.min_version = "0";
|
||||
return os;
|
||||
};
|
||||
|
||||
const validateLocationCheck = (locationCheck?: GeoLocationCheck) => {
|
||||
if (!locationCheck) return;
|
||||
if (!locationCheck.locations) return;
|
||||
return {
|
||||
action: locationCheck.action,
|
||||
locations: locationCheck.locations.map((location) => {
|
||||
if (location.city_name == "")
|
||||
return { country_code: location.country_code };
|
||||
return {
|
||||
country_code: location.country_code,
|
||||
city_name: location.city_name,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const updateOrCreatePostureCheck = () => {
|
||||
const newData = {
|
||||
name,
|
||||
description,
|
||||
checks: {
|
||||
nb_version_check: nbVersionCheck,
|
||||
geo_location_check: validateLocationCheck(geoLocationCheck),
|
||||
os_version_check: validateOSCheck(osVersionCheck),
|
||||
},
|
||||
};
|
||||
|
||||
const updateOrCreate = !postureCheck
|
||||
? () =>
|
||||
postureCheckRequest.post(newData).then((check: PostureCheck) => {
|
||||
mutate("/posture-checks");
|
||||
onSuccess?.(check);
|
||||
onOpenChange(false);
|
||||
})
|
||||
: () =>
|
||||
postureCheckRequest
|
||||
.put({ ...newData, id: postureCheck.id }, `/${postureCheck.id}`)
|
||||
.then((check: PostureCheck) => {
|
||||
mutate("/posture-checks").then();
|
||||
onSuccess?.(check);
|
||||
onOpenChange(false);
|
||||
});
|
||||
|
||||
notify({
|
||||
title: `Posture Check ${newData.name}`,
|
||||
description: `Posture Check was ${
|
||||
postureCheck ? "updated" : "created"
|
||||
} successfully.`,
|
||||
loadingMessage: `${
|
||||
postureCheck ? "Updating" : "Creating"
|
||||
} your posture check...`,
|
||||
promise: updateOrCreate(),
|
||||
});
|
||||
};
|
||||
|
||||
const isAtLeastOneCheckEnabled =
|
||||
!!nbVersionCheck || !!geoLocationCheck || !!osVersionCheck;
|
||||
const canCreate = !isEmpty(name) && isAtLeastOneCheckEnabled;
|
||||
|
||||
const [tab, setTab] = useState("checks");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative", "max-w-2xl")}
|
||||
showClose={true}
|
||||
>
|
||||
<ModalHeader
|
||||
icon={<ShieldCheck size={19} />}
|
||||
title={
|
||||
postureCheck ? "Update Posture Check" : "Create Posture Check"
|
||||
}
|
||||
description={
|
||||
"Use posture checks to further restrict access in your network."
|
||||
}
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs onValueChange={(v) => setTab(v)} defaultValue={tab} value={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"checks"}>
|
||||
<LayoutList size={16} />
|
||||
Checks
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value={"general"}>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Name & Description
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={"checks"} className={"pb-6 px-8"}>
|
||||
<>
|
||||
<PostureCheckNetBirdVersion
|
||||
value={nbVersionCheck}
|
||||
onChange={setNbVersionCheck}
|
||||
/>
|
||||
<PostureCheckGeoLocation
|
||||
value={geoLocationCheck}
|
||||
onChange={setGeoLocationCheckCheck}
|
||||
/>
|
||||
<PostureCheckOperatingSystem
|
||||
value={osVersionCheck}
|
||||
onChange={setOsVersionCheck}
|
||||
/>
|
||||
</>
|
||||
</TabsContent>
|
||||
<TabsContent value={"general"} className={"pb-8 px-8"}>
|
||||
<div className={"flex flex-col gap-6"}>
|
||||
<div>
|
||||
<Label>Name of the Posture Check</Label>
|
||||
<HelpText>
|
||||
Set an easily identifiable name for your posture check.
|
||||
</HelpText>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={"e.g., NetBird Version > 0.25.0"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description (optional)</Label>
|
||||
<HelpText>
|
||||
Write a short description to add more context to this
|
||||
policy.
|
||||
</HelpText>
|
||||
<Textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={
|
||||
"e.g., Check if the NetBird version is bigger than 0.25.0"
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Posture Checks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{!postureCheck && tab == "checks" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={!isAtLeastOneCheckEnabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{((!postureCheck && tab == "general") || postureCheck) && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canCreate}
|
||||
onClick={updateOrCreatePostureCheck}
|
||||
>
|
||||
{postureCheck ? "Save Changes" : "Create Posture Check"}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
131
src/modules/posture-checks/table/PostureCheckBrowseTable.tsx
Normal file
131
src/modules/posture-checks/table/PostureCheckBrowseTable.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import Button from "@components/Button";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import type { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckChecksCell } from "@/modules/posture-checks/table/cells/PostureCheckChecksCell";
|
||||
import { PostureCheckNameCell } from "@/modules/posture-checks/table/cells/PostureCheckNameCell";
|
||||
|
||||
type Props = {
|
||||
onAdd: (checks: PostureCheck[]) => void;
|
||||
};
|
||||
|
||||
export default function PostureCheckBrowseTable({ onAdd }: Props) {
|
||||
const { data: postureChecks, isLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
const { mutate } = useSWRConfig();
|
||||
const path = usePathname();
|
||||
|
||||
// Default sorting state of the table
|
||||
const [sorting, setSorting] = useLocalStorage<SortingState>(
|
||||
"netbird-table-sort" + path,
|
||||
[
|
||||
{
|
||||
id: "name",
|
||||
desc: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={""}>
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Posture Check"}
|
||||
sorting={sorting}
|
||||
wrapperClassName={""}
|
||||
setSorting={setSorting}
|
||||
columns={PostureChecksColumns}
|
||||
showHeader={true}
|
||||
columnVisibility={{
|
||||
description: false,
|
||||
}}
|
||||
tableClassName={"mt-6 !border-0"}
|
||||
rowClassName={"!border-b-0 px-10"}
|
||||
data={postureChecks}
|
||||
searchPlaceholder={"Search by name and description..."}
|
||||
onRowClick={(row) => row.toggleSelected()}
|
||||
rightSide={(table) => (
|
||||
<>
|
||||
{postureChecks && postureChecks?.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() =>
|
||||
onAdd(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
}
|
||||
disabled={table.getSelectedRowModel().rows.length <= 0}
|
||||
>
|
||||
Add Posture Checks ({table.getSelectedRowModel().rows.length})
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{() => {
|
||||
return (
|
||||
<>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={postureChecks?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/posture-checks");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const PostureChecksColumns: ColumnDef<PostureCheck>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<div className={"min-w-[20px] max-w-[20px]"}>
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className={"min-w-[20px] max-w-[20px]"}>
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<PostureCheckNameCell small={true} check={row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Checks</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <PostureCheckChecksCell check={row.original} />,
|
||||
},
|
||||
];
|
||||
121
src/modules/posture-checks/table/PostureCheckMinimalTable.tsx
Normal file
121
src/modules/posture-checks/table/PostureCheckMinimalTable.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Label } from "@components/Label";
|
||||
import {
|
||||
Edit,
|
||||
FolderSearch,
|
||||
MinusCircleIcon,
|
||||
MoreVertical,
|
||||
PlusCircle,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckChecksCell } from "@/modules/posture-checks/table/cells/PostureCheckChecksCell";
|
||||
import { PostureCheckNameCell } from "@/modules/posture-checks/table/cells/PostureCheckNameCell";
|
||||
import { PostureCheckNoChecksInfo } from "@/modules/posture-checks/ui/PostureCheckNoChecksInfo";
|
||||
|
||||
type Props = {
|
||||
data: PostureCheck[];
|
||||
onAddClick: () => void;
|
||||
onBrowseClick: () => void;
|
||||
onRemoveClick: (check: PostureCheck) => void;
|
||||
onEditClick: (check: PostureCheck) => void;
|
||||
};
|
||||
|
||||
export default function PostureCheckMinimalTable({
|
||||
data,
|
||||
onAddClick,
|
||||
onBrowseClick,
|
||||
onRemoveClick,
|
||||
onEditClick,
|
||||
}: Props) {
|
||||
return data && data.length > 0 ? (
|
||||
<div className={""}>
|
||||
<div className={"flex justify-between gap-10 mb-5 items-end"}>
|
||||
<div>
|
||||
<Label>
|
||||
{data.length}{" "}
|
||||
{data.length == 1 ? "Posture Check" : "Posture Checks"}
|
||||
</Label>
|
||||
<HelpText className={"mb-0"}>
|
||||
Use posture checks to further restrict access in your network.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"flex items-center justify-center gap-4"}>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onBrowseClick}>
|
||||
<FolderSearch size={14} />
|
||||
Browse Checks
|
||||
</Button>
|
||||
<Button variant={"primary"} size={"xs"} onClick={onAddClick}>
|
||||
<PlusCircle size={14} />
|
||||
New Posture Check
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
"rounded-md overflow-hidden border border-nb-gray-900 bg-nb-gray-920/30 py-1 px-1"
|
||||
}
|
||||
>
|
||||
{data.map((check) => {
|
||||
return (
|
||||
<div
|
||||
key={check.id}
|
||||
className={
|
||||
"flex justify-between py-2 items-center hover:bg-nb-gray-900/30 rounded-md cursor-pointer px-4 transition-all"
|
||||
}
|
||||
onClick={() => onEditClick(check)}
|
||||
>
|
||||
<PostureCheckNameCell small={true} check={check} />
|
||||
<div className={"flex gap-4 items-center"}>
|
||||
<PostureCheckChecksCell check={check} />
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
asChild={true}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button variant={"default-outline"} className={"!px-3"}>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-auto min-w-[200px]"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => onEditClick(check)}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Edit size={14} className={"shrink-0"} />
|
||||
Edit Posture Check
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onRemoveClick(check)}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<MinusCircleIcon size={14} className={"shrink-0"} />
|
||||
Remove Posture Check
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PostureCheckNoChecksInfo
|
||||
onAddClick={onAddClick}
|
||||
onBrowseClick={onBrowseClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
235
src/modules/posture-checks/table/PostureCheckTable.tsx
Normal file
235
src/modules/posture-checks/table/PostureCheckTable.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import Button from "@components/Button";
|
||||
import ButtonGroup from "@components/ButtonGroup";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import type { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import PostureCheckModal from "@/modules/posture-checks/modal/PostureCheckModal";
|
||||
import { PostureCheckActionCell } from "@/modules/posture-checks/table/cells/PostureCheckActionCell";
|
||||
import { PostureCheckChecksCell } from "@/modules/posture-checks/table/cells/PostureCheckChecksCell";
|
||||
import { PostureCheckNameCell } from "@/modules/posture-checks/table/cells/PostureCheckNameCell";
|
||||
import { PostureCheckPolicyUsageCell } from "@/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell";
|
||||
|
||||
type Props = {
|
||||
isLoading: boolean;
|
||||
postureChecks: PostureCheck[] | undefined;
|
||||
};
|
||||
|
||||
const Columns: ColumnDef<PostureCheck>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <PostureCheckNameCell check={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "active",
|
||||
accessorKey: "active",
|
||||
sortingFn: "basic",
|
||||
},
|
||||
{
|
||||
id: "checks",
|
||||
accessorFn: (row) => Object.keys(row.checks).length,
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Checks</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <PostureCheckChecksCell check={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "access_control_usage",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Used by</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <PostureCheckPolicyUsageCell check={row.original} />,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => <PostureCheckActionCell check={row.original} />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function PostureCheckTable({ postureChecks, isLoading }: Props) {
|
||||
const { data: policies } = useFetchApi<Policy[]>("/policies");
|
||||
const { mutate } = useSWRConfig();
|
||||
const path = usePathname();
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!postureChecks) return [];
|
||||
return postureChecks?.map((check) => {
|
||||
const checkId = check.id;
|
||||
if (!policies) return check;
|
||||
const usage = policies?.filter((policy) => {
|
||||
if (!policy.source_posture_checks) return false;
|
||||
return policy.source_posture_checks.includes(checkId);
|
||||
});
|
||||
const isOnePolicyEnabled = usage.some((policy) => policy.enabled);
|
||||
return {
|
||||
...check,
|
||||
policies: usage || [],
|
||||
active: isOnePolicyEnabled,
|
||||
};
|
||||
});
|
||||
}, [postureChecks, policies]);
|
||||
|
||||
// Default sorting state of the table
|
||||
const [sorting, setSorting] = useLocalStorage<SortingState>(
|
||||
"netbird-table-sort" + path,
|
||||
[
|
||||
{
|
||||
id: "active",
|
||||
desc: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const [postureCheckModal, setPostureCheckModal] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<PostureCheck>();
|
||||
const [, setCurrentCellClicked] = useState("");
|
||||
|
||||
return (
|
||||
<div className={""}>
|
||||
{postureCheckModal && (
|
||||
<PostureCheckModal
|
||||
open={postureCheckModal}
|
||||
key={currentRow ? 1 : 0}
|
||||
onOpenChange={setPostureCheckModal}
|
||||
postureCheck={currentRow}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Posture Check"}
|
||||
sorting={sorting}
|
||||
wrapperClassName={""}
|
||||
setSorting={setSorting}
|
||||
columns={Columns}
|
||||
showHeader={true}
|
||||
columnVisibility={{
|
||||
active: false,
|
||||
}}
|
||||
onRowClick={(row, cell) => {
|
||||
setCurrentRow(row.original);
|
||||
setPostureCheckModal(true);
|
||||
setCurrentCellClicked(cell);
|
||||
}}
|
||||
data={data}
|
||||
searchPlaceholder={"Search by name and description..."}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{data && data?.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setPostureCheckModal(true);
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Add Posture Check
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<ShieldCheck size={23} />}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Create Posture Check"}
|
||||
description={
|
||||
"Add posture checks to further restrict access in your network. E.g., only clients with a specific NetBird client version, operating system or location are allowed to connect."
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => setPostureCheckModal(true)}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Create Posture Check
|
||||
</Button>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more about
|
||||
<InlineLink href={"#"} target={"_blank"}>
|
||||
Posture Checks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(table) => {
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup disabled={data?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("active")?.setFilterValue(true);
|
||||
}}
|
||||
disabled={data?.length == 0}
|
||||
variant={
|
||||
table.getColumn("active")?.getFilterValue() == true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Active
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("active")?.setFilterValue("");
|
||||
}}
|
||||
disabled={data?.length == 0}
|
||||
variant={
|
||||
table.getColumn("active")?.getFilterValue() != true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={data?.length == 0}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={data?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/posture-checks");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import Button from "@components/Button";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
};
|
||||
export const PostureCheckActionCell = ({ check }: Props) => {
|
||||
const deleteRequest = useApiCall("/posture-checks");
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const handleDelete = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Delete '${check.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this posture check? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
if (choice) {
|
||||
notify({
|
||||
title: check.name,
|
||||
description: "Posture check was successfully deleted",
|
||||
promise: deleteRequest.del({}, `/${check.id}`).then(() => {
|
||||
mutate("/posture-checks").then();
|
||||
}),
|
||||
loadingMessage: "Deleting posture check...",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasPolicies = check.policies ? check.policies?.length > 0 : false;
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
<FullTooltip
|
||||
disabled={!hasPolicies}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
This posture check is assigned to a policy and cannot be deleted.
|
||||
Please remove the posture check from all policies before deleting
|
||||
it.
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={handleDelete}
|
||||
disabled={hasPolicies}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Disc3Icon, FlagIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { GeoLocationTooltip } from "@/modules/posture-checks/checks/tooltips/GeoLocationTooltip";
|
||||
import { NetBirdVersionTooltip } from "@/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip";
|
||||
import { OperatingSystemTooltip } from "@/modules/posture-checks/checks/tooltips/OperatingSystemTooltip";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
};
|
||||
export const PostureCheckChecksCell = ({ check }: Props) => {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-3 bg-nb-gray-900/80 hover:bg-nb-gray-800 border border-nb-gray-800/50 py-1 rounded-full px-1 transition-all"
|
||||
}
|
||||
>
|
||||
<div className={"flex -space-x-2 "}>
|
||||
{check.checks.nb_version_check && (
|
||||
<NetBirdVersionTooltip
|
||||
version={check.checks.nb_version_check.min_version}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gradient-to-tr from-netbird-200 to-netbird-100 h-8 w-8 rounded-full flex items-center justify-center relative z-[10] hover:scale-[1.1] transition-all",
|
||||
)}
|
||||
>
|
||||
<NetBirdIcon size={14} />
|
||||
</div>
|
||||
</NetBirdVersionTooltip>
|
||||
)}
|
||||
|
||||
{check.checks.geo_location_check && (
|
||||
<GeoLocationTooltip check={check.checks.geo_location_check}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gradient-to-tr from-indigo-500 to-indigo-400 h-8 w-8 rounded-full flex items-center justify-center relative z-[9] hover:scale-[1.1] transition-all",
|
||||
)}
|
||||
>
|
||||
<FlagIcon size={14} />
|
||||
</div>
|
||||
</GeoLocationTooltip>
|
||||
)}
|
||||
|
||||
{check.checks.os_version_check && (
|
||||
<OperatingSystemTooltip check={check.checks.os_version_check}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300 h-8 w-8 rounded-full flex items-center justify-center relative z-[8] hover:scale-[1.1] transition-all",
|
||||
)}
|
||||
>
|
||||
<Disc3Icon size={14} />
|
||||
</div>
|
||||
</OperatingSystemTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
};
|
||||
export const PostureCheckLocationCell = ({ check }: Props) => {
|
||||
const countries = check.checks.geo_location_check?.locations.map(
|
||||
(location) => location.country_code,
|
||||
);
|
||||
|
||||
return countries ? (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<span className={"font-medium text-nb-gray-200"}></span>
|
||||
{countries.map((country, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={"relative"}
|
||||
style={{
|
||||
left: -index * 15,
|
||||
}}
|
||||
>
|
||||
<RoundedFlag country={country} size={23} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyRow />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import DescriptionWithTooltip from "@components/ui/DescriptionWithTooltip";
|
||||
import * as React from "react";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import ActiveInactiveRow from "@/modules/common-table-rows/ActiveInactiveRow";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
small?: boolean;
|
||||
};
|
||||
export const PostureCheckNameCell = ({ check, small }: Props) => {
|
||||
return !small ? (
|
||||
<ActiveInactiveRow
|
||||
active={check.active || false}
|
||||
inactiveDot={"gray"}
|
||||
text={check.name}
|
||||
>
|
||||
<DescriptionWithTooltip
|
||||
className={"mt-1"}
|
||||
text={check.description}
|
||||
maxChars={30}
|
||||
/>
|
||||
</ActiveInactiveRow>
|
||||
) : (
|
||||
<div className={"flex items-center gap-4 min-w-[350px]"}>
|
||||
<div className={"flex flex-col gap-0.5 min-w-0 max-w-[300px]"}>
|
||||
<div className={"text-sm text-nb-gray-100 truncate"}>{check.name}</div>
|
||||
<DescriptionWithTooltip
|
||||
className={"text-xs"}
|
||||
text={check.description}
|
||||
maxChars={30}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ArrowUpRightSquareIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
};
|
||||
export const PostureCheckPolicyUsageCell = ({ check }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-4")}>
|
||||
<FullTooltip
|
||||
disabled={!(check.policies && check.policies?.length > 0)}
|
||||
content={
|
||||
<div className={"text-xs max-w-lg"}>
|
||||
<span className={"font-medium text-nb-gray-100 text-sm"}>
|
||||
Assigned
|
||||
{check.policies && check.policies?.length > 1
|
||||
? " Policies"
|
||||
: " Policy"}
|
||||
</span>
|
||||
<div className={"flex gap-2 pt-3 pb-2 flex-wrap"}>
|
||||
{check.policies &&
|
||||
check.policies?.length > 0 &&
|
||||
check.policies?.map((policy: Policy, index: number) => {
|
||||
return (
|
||||
<Badge
|
||||
variant={"gray-ghost"}
|
||||
useHover={false}
|
||||
key={index}
|
||||
className={"justify-start font-medium"}
|
||||
>
|
||||
<AccessControlIcon size={12} />
|
||||
{policy.name}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
>
|
||||
<Badge
|
||||
onClick={() => router.push("/access-control")}
|
||||
variant={"gray"}
|
||||
useHover={!!(check.policies && check.policies?.length > 0)}
|
||||
className={cn(
|
||||
"min-w-[110px] font-medium cursor-pointer",
|
||||
check.policies &&
|
||||
check.policies.length == 0 &&
|
||||
"opacity-30 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<AccessControlIcon size={12} />
|
||||
<span>
|
||||
<span className={"font-bold"}>
|
||||
{check.policies && check.policies?.length > 0
|
||||
? check.policies && check.policies?.length
|
||||
: ""}
|
||||
</span>{" "}
|
||||
{check.policies && check.policies?.length == 0
|
||||
? "No Policies"
|
||||
: check.policies && check.policies?.length > 1
|
||||
? "Policies"
|
||||
: "Policy"}
|
||||
</span>
|
||||
</Badge>
|
||||
</FullTooltip>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-[260px]"}>
|
||||
To assign this posture check to your policies, visit the Policies
|
||||
page.
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
>
|
||||
<Button
|
||||
size={"xs"}
|
||||
variant={"secondary"}
|
||||
className={"min-w-[130px]"}
|
||||
onClick={() => router.push("/access-control")}
|
||||
>
|
||||
<>
|
||||
<ArrowUpRightSquareIcon size={12} />
|
||||
Go to Policies
|
||||
</>
|
||||
</Button>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
154
src/modules/posture-checks/ui/PostureCheckCard.tsx
Normal file
154
src/modules/posture-checks/ui/PostureCheckCard.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { Modal, ModalContent } from "@components/modal/Modal";
|
||||
import { IconCircleFilled } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ScaleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
|
||||
export const PostureCheckCard = ({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconClass = "bg-gradient-to-tr from-netbird-200 to-netbird-100",
|
||||
modalWidthClass = "max-w-xl",
|
||||
onClose,
|
||||
open,
|
||||
setOpen,
|
||||
active,
|
||||
onReset,
|
||||
license,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
iconClass?: string;
|
||||
icon?: React.ReactNode;
|
||||
modalWidthClass?: string;
|
||||
onClose?: () => void;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onReset?: () => void;
|
||||
active?: boolean;
|
||||
license?: React.ReactNode;
|
||||
}) => {
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const handleReset = async () => {
|
||||
const reset = await confirm({
|
||||
title: `Disable this check?`,
|
||||
description:
|
||||
"Are you sure you want to disable this check? All settings of this check will be lost.",
|
||||
confirmText: "Disable",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
if (reset) onReset?.();
|
||||
};
|
||||
|
||||
const licenseToolTip = (
|
||||
<FullTooltip content={license}>
|
||||
<ScaleIcon
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-400 hover:text-nb-gray-200 transition-all cursor-pointer -top-[1px] relative"
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"w-full"}>
|
||||
<div
|
||||
onClick={() => setOpen(true)}
|
||||
className={
|
||||
"hover:bg-nb-gray-920/80 border border-transparent hover:border-nb-gray-900 rounded-md flex flex-col items-center transition-all cursor-pointer w-full"
|
||||
}
|
||||
>
|
||||
<div className={"flex gap-4 items-center w-full px-4 py-3"}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 shrink-0 shadow-xl rounded-md flex items-center justify-center select-none",
|
||||
iconClass,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={" w-full"}>
|
||||
<div
|
||||
className={
|
||||
"text-sm font-medium flex gap-2 items-center justify-between"
|
||||
}
|
||||
>
|
||||
<span className={"flex items-center gap-2"}>
|
||||
{title}
|
||||
{license && licenseToolTip}
|
||||
</span>
|
||||
</div>
|
||||
<div className={"text-xs mt-0.5 text-nb-gray-300"}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] rounded-full px-1 py-1 flex items-center gap-1 w-[50px] justify-center uppercase font-medium",
|
||||
active
|
||||
? "text-green-400 bg-green-900 hover:bg-green-800 transition-all hover:text-green-200"
|
||||
: "text-nb-gray-400 bg-nb-gray-900",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (active) handleReset().then();
|
||||
else setOpen(true);
|
||||
}}
|
||||
>
|
||||
<IconCircleFilled size={7} className={"mt-[0.1px]"} />
|
||||
{active ? "On" : "Off"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
if (onClose && !open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
key={open ? 1 : 0}
|
||||
>
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative", modalWidthClass)}
|
||||
showClose={true}
|
||||
>
|
||||
<div className={"flex gap-4 items-center px-8 pb-5"}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 shrink-0 shadow-xl rounded-md flex items-center justify-center select-none",
|
||||
iconClass,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={"pr-10"}>
|
||||
<div className={"text-sm font-medium flex gap-2 items-center"}>
|
||||
{title}
|
||||
{license && licenseToolTip}
|
||||
</div>
|
||||
<div className={"text-xs mt-0.5 text-nb-gray-300"}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
60
src/modules/posture-checks/ui/PostureCheckIcons.tsx
Normal file
60
src/modules/posture-checks/ui/PostureCheckIcons.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
import { CountryDERounded } from "@/assets/countries/CountryDERounded";
|
||||
import { CountryUSRounded } from "@/assets/countries/CountryUSRounded";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import AppleLogo from "@/assets/os-icons/apple.svg";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const PostureCheckIcons = () => {
|
||||
return (
|
||||
<div className={"flex items-center justify-center -space-x-2"}>
|
||||
<Circle className={"top-2"}>
|
||||
<Image src={AppleLogo} alt={""} width={14} />
|
||||
</Circle>
|
||||
<Circle className={"top-1"}>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-6 overflow-hidden rounded-full flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<CountryDERounded />
|
||||
</div>
|
||||
</Circle>
|
||||
<Circle className={"z-[3]"}>
|
||||
<NetBirdIcon size={18} />
|
||||
</Circle>
|
||||
<Circle className={"top-1 z-[2]"}>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-6 overflow-hidden rounded-full flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<CountryUSRounded />
|
||||
</div>
|
||||
</Circle>
|
||||
<Circle className={"z-[1] top-2 "}>
|
||||
<FaWindows className={"text-white text-md"} />
|
||||
</Circle>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Circle = ({ children, className }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full bg-nb-gray-900 flex items-center justify-center relative border-2 border-nb-gray",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
53
src/modules/posture-checks/ui/PostureCheckNoChecksInfo.tsx
Normal file
53
src/modules/posture-checks/ui/PostureCheckNoChecksInfo.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import Button from "@components/Button";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { FolderSearch } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
export function PostureCheckNoChecksInfo({
|
||||
onAddClick,
|
||||
onBrowseClick,
|
||||
}: {
|
||||
onAddClick: () => void;
|
||||
onBrowseClick: () => void;
|
||||
}) {
|
||||
const { data: postureChecks } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"mx-auto text-center flex flex-col items-center justify-center"
|
||||
}
|
||||
>
|
||||
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
|
||||
{"You haven't added any posture checks yet"}
|
||||
</h2>
|
||||
<Paragraph className={cn("text-sm text-center max-w-md mt-1")}>
|
||||
Add various posture checks to further restrict access in your network.
|
||||
E.g., only clients with a specific NetBird client version, operating
|
||||
system or location are allowed to connect.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex items-center justify-center gap-4 mt-5"}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
disabled={postureChecks?.length == 0}
|
||||
onClick={onBrowseClick}
|
||||
>
|
||||
<FolderSearch size={14} />
|
||||
Browse Checks
|
||||
</Button>
|
||||
<Button variant={"primary"} size={"xs"} onClick={onAddClick}>
|
||||
<IconCirclePlus size={14} />
|
||||
New Posture Check
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { SelectOption } from "@components/select/SelectDropdown";
|
||||
import { IconMathEqualGreater } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
version?: string;
|
||||
versionText?: string;
|
||||
versionList?: SelectOption[];
|
||||
icon: React.FunctionComponent<{ size: number }>;
|
||||
os: string;
|
||||
};
|
||||
export const PostureCheckOperatingSystemInfo = ({
|
||||
version,
|
||||
icon,
|
||||
versionText = "Version",
|
||||
versionList,
|
||||
os,
|
||||
}: Props) => {
|
||||
const operatingSystemName = versionList?.find(
|
||||
(item) => item.value === version,
|
||||
)?.label;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 pl-1 pr-4 py-1 text-xs justify-start",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-5 h-5 flex items-center justify-center brightness-[150%]",
|
||||
)}
|
||||
>
|
||||
{icon({ size: 14 })}
|
||||
</div>
|
||||
<div className={"flex items-center"}>
|
||||
<span
|
||||
className={cn(
|
||||
version ? "text-green-500" : "text-red-500",
|
||||
"mr-1 font-semibold",
|
||||
)}
|
||||
>
|
||||
{version ? "Allow" : "Block"}{" "}
|
||||
</span>
|
||||
|
||||
{version ? (
|
||||
version == "0" ? (
|
||||
os
|
||||
) : (
|
||||
<div className={"flex items-center gap-1"}>
|
||||
{" "}
|
||||
{os} {operatingSystemName ? "Version" : versionText}
|
||||
<span
|
||||
className={"text-netbird flex items-center gap-1 font-semibold"}
|
||||
>
|
||||
<IconMathEqualGreater size={14} />
|
||||
{operatingSystemName || version}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
os
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
95
src/modules/posture-checks/ui/PostureCheckTab.tsx
Normal file
95
src/modules/posture-checks/ui/PostureCheckTab.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { TabsContent } from "@components/Tabs";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckBrowseModal } from "@/modules/posture-checks/modal/PostureCheckBrowseModal";
|
||||
import PostureCheckModal from "@/modules/posture-checks/modal/PostureCheckModal";
|
||||
import PostureCheckMinimalTable from "@/modules/posture-checks/table/PostureCheckMinimalTable";
|
||||
|
||||
type Props = {
|
||||
postureChecks: PostureCheck[];
|
||||
setPostureChecks: React.Dispatch<React.SetStateAction<PostureCheck[]>>;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export const PostureCheckTab = ({
|
||||
postureChecks,
|
||||
setPostureChecks,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const addPostureChecks = (checks: PostureCheck[]) => {
|
||||
setPostureChecks((prev) => {
|
||||
const previous = prev.map((check) => {
|
||||
const find = checks.find((c) => c.id === check.id);
|
||||
if (find) return find;
|
||||
return check;
|
||||
});
|
||||
const allChecks = [...previous, ...checks];
|
||||
return allChecks.filter(
|
||||
(check, index, self) =>
|
||||
self.findIndex((c) => c.id === check.id) === index,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const removePostureCheck = (check: PostureCheck) => {
|
||||
setPostureChecks((prev) => {
|
||||
return prev.filter((c) => c.id !== check.id);
|
||||
});
|
||||
};
|
||||
|
||||
const [checkModal, setCheckModal] = useState(false);
|
||||
const [browseModal, setBrowseModal] = useState(false);
|
||||
const [currentEditCheck, setCurrentEditCheck] = useState<PostureCheck>();
|
||||
|
||||
return isLoading ? (
|
||||
<TabsContent
|
||||
value={"posture_checks"}
|
||||
className={"px-8 pb-8 mt-3 gap-2 flex flex-col"}
|
||||
>
|
||||
<Skeleton width={"100%"} height={41} />
|
||||
<Skeleton width={"100%"} height={42} />
|
||||
<Skeleton width={"100%"} height={42} />
|
||||
<Skeleton width={"100%"} height={41} />
|
||||
</TabsContent>
|
||||
) : (
|
||||
<TabsContent value={"posture_checks"} className={"px-8 pb-8 mt-3"}>
|
||||
{checkModal && (
|
||||
<PostureCheckModal
|
||||
open={checkModal}
|
||||
onOpenChange={setCheckModal}
|
||||
onSuccess={(check) => addPostureChecks([check])}
|
||||
postureCheck={currentEditCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
{browseModal && (
|
||||
<PostureCheckBrowseModal
|
||||
open={browseModal}
|
||||
onOpenChange={setBrowseModal}
|
||||
onSuccess={(check) => addPostureChecks(check)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={"flex flex-col gap-3"}>
|
||||
<PostureCheckMinimalTable
|
||||
data={postureChecks}
|
||||
onEditClick={(check) => {
|
||||
setCurrentEditCheck(check);
|
||||
setCheckModal(true);
|
||||
}}
|
||||
onAddClick={() => {
|
||||
setCurrentEditCheck(undefined);
|
||||
setCheckModal(true);
|
||||
}}
|
||||
onBrowseClick={() => {
|
||||
setCurrentEditCheck(undefined);
|
||||
setBrowseModal(true);
|
||||
}}
|
||||
onRemoveClick={removePostureCheck}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
);
|
||||
};
|
||||
12
src/modules/posture-checks/ui/PostureCheckTabTrigger.tsx
Normal file
12
src/modules/posture-checks/ui/PostureCheckTabTrigger.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TabsTrigger } from "@components/Tabs";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export const PostureCheckTabTrigger = () => {
|
||||
return (
|
||||
<TabsTrigger value={"posture_checks"}>
|
||||
<ShieldCheck size={16} />
|
||||
Posture Checks
|
||||
</TabsTrigger>
|
||||
);
|
||||
};
|
||||
@@ -69,4 +69,9 @@ export const validator = {
|
||||
); // validate fragment locator
|
||||
return urlPattern.test(urlString);
|
||||
},
|
||||
isValidVersion: (version: string) => {
|
||||
const semverRegex =
|
||||
/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
||||
return semverRegex.test(version);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user