mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fb379abf | ||
|
|
831673d0d6 |
No files matched your search
@@ -71,7 +71,6 @@ jobs:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.NB_DOCKER_USER }}
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -66,8 +66,8 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
||||
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
|
||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
@@ -350,15 +350,15 @@ const PeerGeneralInformation = () => {
|
||||
/>
|
||||
</FullTooltip>
|
||||
|
||||
{/* Remote Access Buttons */}
|
||||
<div>
|
||||
<Label>Remote Access</Label>
|
||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
||||
<div className="flex gap-3">
|
||||
<SSHButton peer={peer} />
|
||||
<RDPButton peer={peer} />
|
||||
</div>
|
||||
{/* Remote Access Buttons */}
|
||||
<div>
|
||||
<Label>Remote Access</Label>
|
||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
||||
<div className="flex gap-3">
|
||||
<SSHButton peer={peer} />
|
||||
<RDPButton peer={peer} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{permission.groups.read && (
|
||||
<div>
|
||||
@@ -582,6 +582,23 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{peer.created_at && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<CalendarDays size={16} />
|
||||
Registered on
|
||||
</>
|
||||
}
|
||||
value={
|
||||
dayjs(peer.created_at).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.created_at) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function NotFound() {
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
const params = queryParams && `?${queryParams}`;
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
@@ -58,6 +58,8 @@ export default function OIDCProvider({ children }: Props) {
|
||||
"utm_content",
|
||||
"utm_campaign",
|
||||
"hs_id",
|
||||
"page",
|
||||
"page_size",
|
||||
"user",
|
||||
"port",
|
||||
];
|
||||
|
||||
@@ -34,7 +34,7 @@ export const buttonVariants = cva(
|
||||
secondary: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
||||
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
],
|
||||
secondaryLighter: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
|
||||
@@ -25,6 +25,8 @@ type Props = {
|
||||
customOnOpenChange?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
delayDuration?: number;
|
||||
skipDelayDuration?: number;
|
||||
alignOffset?: number;
|
||||
sideOffset?: number;
|
||||
} & TooltipProps &
|
||||
TooltipVariants;
|
||||
|
||||
@@ -45,6 +47,8 @@ export default function FullTooltip({
|
||||
delayDuration = 1,
|
||||
skipDelayDuration = 300,
|
||||
variant = "default",
|
||||
alignOffset = 20,
|
||||
sideOffset,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(!!keepOpen);
|
||||
|
||||
@@ -83,7 +87,8 @@ export default function FullTooltip({
|
||||
)}
|
||||
{!disabled && (
|
||||
<TooltipContent
|
||||
alignOffset={20}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
forceMount={true}
|
||||
className={contentClassName}
|
||||
variant={variant}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
export const ListItem = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className={"text-nb-gray-300"}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import AddPeerButton from "@components/ui/AddPeerButton";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
|
||||
type Props = {
|
||||
showBackground?: boolean;
|
||||
};
|
||||
|
||||
export const NoPeersGettingStarted = ({ showBackground = true }) => {
|
||||
return (
|
||||
<GetStartedTest
|
||||
showBackground={showBackground}
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<PeerIcon className={"fill-nb-gray-200"} size={20} />}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Get Started with NetBird"}
|
||||
description={
|
||||
"It looks like you don't have any connected machines.\n" +
|
||||
"Get started by adding one to your network."
|
||||
}
|
||||
button={<AddPeerButton />}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more in our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Getting Started Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -42,13 +42,13 @@ import { NetworkResource } from "@/interfaces/Network";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { PolicyRuleResource } from "@/interfaces/Policy";
|
||||
import { User } from "@/interfaces/User";
|
||||
import { HorizontalUsersStack } from "@/modules/users/HorizontalUsersStack";
|
||||
import { PeerOperatingSystemIcon } from "@/modules/peers/PeerOperatingSystemIcon";
|
||||
import { HorizontalUsersStack } from "@/modules/users/HorizontalUsersStack";
|
||||
|
||||
const groupsSearchPredicate = (item: Group, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item?.id?.toLowerCase().includes(lowerCaseQuery) ?? false;
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item?.id?.toLowerCase().includes(lowerCaseQuery) ?? false;
|
||||
};
|
||||
|
||||
interface MultiSelectProps {
|
||||
@@ -104,11 +104,11 @@ export function PeerGroupSelector({
|
||||
placeholderForSearch = 'Search groups or add new group by pressing "Enter"...',
|
||||
}: Readonly<MultiSelectProps>) {
|
||||
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
|
||||
NetworkResource[]
|
||||
NetworkResource[]
|
||||
>("/networks/resources");
|
||||
|
||||
const { data: peers, isLoading: isPeersLoading } =
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
|
||||
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
|
||||
useGroups();
|
||||
@@ -118,18 +118,19 @@ export function PeerGroupSelector({
|
||||
const [inputRef, { width }] = useElementSize<
|
||||
HTMLButtonElement | HTMLSpanElement
|
||||
>();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const sortedDropdownOptions = useSortedDropdownOptions(
|
||||
dropdownOptions,
|
||||
values,
|
||||
open,
|
||||
dropdownOptions,
|
||||
values,
|
||||
open,
|
||||
);
|
||||
|
||||
const [filteredGroups, search, setSearch] = useSearch(
|
||||
sortedDropdownOptions,
|
||||
groupsSearchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
sortedDropdownOptions,
|
||||
groupsSearchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
);
|
||||
|
||||
// Update dropdown options when groups change
|
||||
@@ -247,10 +248,10 @@ export function PeerGroupSelector({
|
||||
}, [tab]);
|
||||
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (tab === "groups") return placeholderForSearch;
|
||||
if (tab === "resources") return "Search resource...";
|
||||
if (tab === "peers") return "Search peer...";
|
||||
return "Search...";
|
||||
if (tab === "groups") return placeholderForSearch;
|
||||
if (tab === "resources") return "Search resource...";
|
||||
if (tab === "peers") return "Search peer...";
|
||||
return "Search...";
|
||||
}, [tab, placeholderForSearch]);
|
||||
|
||||
const selectResource = (resource?: NetworkResource) => {
|
||||
@@ -266,12 +267,12 @@ export function PeerGroupSelector({
|
||||
};
|
||||
|
||||
const selectPeer = (peer?: Peer) => {
|
||||
if (!peer?.id) return;
|
||||
onResourceChange?.({
|
||||
id: peer.id,
|
||||
type: "peer",
|
||||
});
|
||||
onChange([]);
|
||||
if (!peer?.id) return;
|
||||
onResourceChange?.({
|
||||
id: peer.id,
|
||||
type: "peer",
|
||||
});
|
||||
onChange([]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -389,7 +390,7 @@ export function PeerGroupSelector({
|
||||
side={side}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Command className={"w-full flex"} loop shouldFilter={false}>
|
||||
<Command className={"w-full flex"} loop shouldFilter={false}>
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
@@ -430,17 +431,17 @@ export function PeerGroupSelector({
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue={"groups"} value={tab} onValueChange={setTab}>
|
||||
<TabTriggers
|
||||
searchRef={searchRef}
|
||||
showPeers={showPeers}
|
||||
showResources={showResources}
|
||||
/>
|
||||
<TabTriggers
|
||||
searchRef={searchRef}
|
||||
showPeers={showPeers}
|
||||
showResources={showResources}
|
||||
/>
|
||||
<TabsContent value={"groups"} className={"p-0 my-0"}>
|
||||
<CommandGroup>
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3",
|
||||
filteredGroups.length == 0 && !search && "py-0",
|
||||
filteredGroups.length == 0 && !search && "py-0",
|
||||
)}
|
||||
>
|
||||
{searchedGroupNotFound && (
|
||||
@@ -453,8 +454,8 @@ export function PeerGroupSelector({
|
||||
value={search}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<Badge variant={"gray-ghost"} className={"h-7"}>
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
<Badge variant={"gray-ghost"} className={"h-7"}>
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
{search}
|
||||
</Badge>
|
||||
<div
|
||||
@@ -510,11 +511,11 @@ export function PeerGroupSelector({
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<GroupBadge
|
||||
group={option}
|
||||
showNewBadge={true}
|
||||
className={"h-7"}
|
||||
/>
|
||||
<GroupBadge
|
||||
group={option}
|
||||
showNewBadge={true}
|
||||
className={"h-7"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center gap-5"}>
|
||||
@@ -533,10 +534,10 @@ export function PeerGroupSelector({
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
<MonitorSmartphoneIcon
|
||||
size={14}
|
||||
className={"shrink-0"}
|
||||
/>
|
||||
<MonitorSmartphoneIcon
|
||||
size={14}
|
||||
className={"shrink-0"}
|
||||
/>
|
||||
{peerCount} Peer(s)
|
||||
</div>
|
||||
) : (
|
||||
@@ -568,17 +569,17 @@ export function PeerGroupSelector({
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
{showPeers && (
|
||||
<TabsContent value={"peers"} className={"p-0 my-0"}>
|
||||
<PeersList
|
||||
search={search}
|
||||
peers={peers}
|
||||
isLoading={isPeersLoading}
|
||||
value={resource}
|
||||
onChange={selectPeer}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
{showPeers && (
|
||||
<TabsContent value={"peers"} className={"p-0 my-0"}>
|
||||
<PeersList
|
||||
search={search}
|
||||
peers={peers}
|
||||
isLoading={isPeersLoading}
|
||||
value={resource}
|
||||
onChange={selectPeer}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</CommandList>
|
||||
</Command>
|
||||
@@ -597,6 +598,7 @@ const TabTriggers = ({
|
||||
showPeers?: boolean;
|
||||
}) => {
|
||||
if (!showResources && !showPeers) return null;
|
||||
|
||||
return (
|
||||
<TabsList justify={"start"} className={"px-3"}>
|
||||
<TabsTrigger
|
||||
@@ -613,37 +615,37 @@ const TabTriggers = ({
|
||||
Groups
|
||||
</TabsTrigger>
|
||||
|
||||
{showResources && (
|
||||
<TabsTrigger
|
||||
value={"resources"}
|
||||
className={"text-[.8rem] font-normal"}
|
||||
onClick={() => searchRef.current?.focus()}
|
||||
>
|
||||
<Layers3Icon
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
size={14}
|
||||
/>
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{showResources && (
|
||||
<TabsTrigger
|
||||
value={"resources"}
|
||||
className={"text-[.8rem] font-normal"}
|
||||
onClick={() => searchRef.current?.focus()}
|
||||
>
|
||||
<Layers3Icon
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
size={14}
|
||||
/>
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
{showPeers && (
|
||||
<TabsTrigger
|
||||
value={"peers"}
|
||||
className={"text-[.8rem] font-normal"}
|
||||
onClick={() => searchRef.current?.focus()}
|
||||
>
|
||||
<MonitorSmartphoneIcon
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
size={14}
|
||||
/>
|
||||
Peers
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{showPeers && (
|
||||
<TabsTrigger
|
||||
value={"peers"}
|
||||
className={"text-[.8rem] font-normal"}
|
||||
onClick={() => searchRef.current?.focus()}
|
||||
>
|
||||
<MonitorSmartphoneIcon
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
size={14}
|
||||
/>
|
||||
Peers
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
};
|
||||
@@ -800,105 +802,105 @@ const ResourcesList = ({
|
||||
};
|
||||
|
||||
const peersSearchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().includes(lowerCaseQuery);
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().includes(lowerCaseQuery);
|
||||
};
|
||||
|
||||
const PeersList = ({
|
||||
search,
|
||||
peers,
|
||||
isLoading,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
search: string;
|
||||
peers?: Peer[];
|
||||
isLoading: boolean;
|
||||
value?: PolicyRuleResource;
|
||||
onChange: (peer: Peer) => void;
|
||||
search,
|
||||
peers,
|
||||
isLoading,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
search: string;
|
||||
peers?: Peer[];
|
||||
isLoading: boolean;
|
||||
value?: PolicyRuleResource;
|
||||
onChange: (peer: Peer) => void;
|
||||
}) => {
|
||||
const [filteredItems, _, setSearch] = useSearch(
|
||||
peers || [],
|
||||
peersSearchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
);
|
||||
const [filteredItems, _, setSearch] = useSearch(
|
||||
peers || [],
|
||||
peersSearchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSearch(search);
|
||||
}, [search, setSearch]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={"max-h-[195px] flex flex-col gap-1 py-2 px-2"}>
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (search != "" && filteredItems.length == 0) {
|
||||
return (
|
||||
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
|
||||
There are no peers matching your search. Please try a different search
|
||||
term.
|
||||
</DropdownInfoText>
|
||||
);
|
||||
}
|
||||
|
||||
if (search == "" && filteredItems.length == 0) {
|
||||
return (
|
||||
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
|
||||
There are no peers available yet. <br />
|
||||
Go to <InlineLink href={"/peers"}>Peers</InlineLink> to add some peers.
|
||||
</DropdownInfoText>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
setSearch(search);
|
||||
}, [search, setSearch]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Radio defaultValue={value?.id} name={"peer"} value={value?.id}>
|
||||
<VirtualScrollAreaList
|
||||
items={filteredItems}
|
||||
onSelect={onChange}
|
||||
itemClassName={"dark:aria-selected:bg-nb-gray-800/20"}
|
||||
renderItem={(res) => {
|
||||
if (!res?.id) return;
|
||||
|
||||
return (
|
||||
<Fragment key={res.id}>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Badge
|
||||
useHover={true}
|
||||
data-cy={"group-badge"}
|
||||
variant={"gray-ghost"}
|
||||
className={cn(
|
||||
"transition-all group whitespace-nowrap h-7 px-2",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<PeerOperatingSystemIcon os={res.os} />
|
||||
<TextWithTooltip text={res?.name || ""} maxChars={20} />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center gap-5"}>
|
||||
<div
|
||||
className={
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
{res.ip}
|
||||
<RadioItem value={res.id} />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Radio>
|
||||
<div className={"max-h-[195px] flex flex-col gap-1 py-2 px-2"}>
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
<Skeleton height={42} className={"rounded-md"} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (search != "" && filteredItems.length == 0) {
|
||||
return (
|
||||
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
|
||||
There are no peers matching your search. Please try a different search
|
||||
term.
|
||||
</DropdownInfoText>
|
||||
);
|
||||
}
|
||||
|
||||
if (search == "" && filteredItems.length == 0) {
|
||||
return (
|
||||
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
|
||||
There are no peers available yet. <br />
|
||||
Go to <InlineLink href={"/peers"}>Peers</InlineLink> to add some peers.
|
||||
</DropdownInfoText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Radio defaultValue={value?.id} name={"peer"} value={value?.id}>
|
||||
<VirtualScrollAreaList
|
||||
items={filteredItems}
|
||||
onSelect={onChange}
|
||||
itemClassName={"dark:aria-selected:bg-nb-gray-800/20"}
|
||||
renderItem={(res) => {
|
||||
if (!res?.id) return;
|
||||
|
||||
return (
|
||||
<Fragment key={res.id}>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Badge
|
||||
useHover={true}
|
||||
data-cy={"group-badge"}
|
||||
variant={"gray-ghost"}
|
||||
className={cn(
|
||||
"transition-all group whitespace-nowrap h-7 px-2",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<PeerOperatingSystemIcon os={res.os} />
|
||||
<TextWithTooltip text={res?.name || ""} maxChars={20} />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center gap-5"}>
|
||||
<div
|
||||
className={
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
{res.ip}
|
||||
<RadioItem value={res.id} />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Radio>
|
||||
);
|
||||
};
|
||||
@@ -44,10 +44,12 @@ function Trigger({
|
||||
children,
|
||||
value,
|
||||
disabled = false,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const currentValue = useTabContext();
|
||||
return (
|
||||
@@ -60,6 +62,7 @@ function Trigger({
|
||||
: disabled
|
||||
? ""
|
||||
: "text-nb-gray-400 hover:bg-nb-gray-900/50",
|
||||
className,
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
|
||||
@@ -37,6 +37,10 @@ interface SelectDropdownProps {
|
||||
searchPlaceholder?: string;
|
||||
isLoading?: boolean;
|
||||
variant?: ButtonVariants["variant"];
|
||||
className?: string;
|
||||
size?: "xs" | "sm";
|
||||
children?: React.ReactNode;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export function SelectDropdown({
|
||||
@@ -51,6 +55,10 @@ export function SelectDropdown({
|
||||
searchPlaceholder = "Search...",
|
||||
isLoading = false,
|
||||
variant = "input",
|
||||
className,
|
||||
size = "sm",
|
||||
children,
|
||||
maxHeight,
|
||||
}: Readonly<SelectDropdownProps>) {
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
|
||||
@@ -79,6 +87,46 @@ export function SelectDropdown({
|
||||
});
|
||||
}, [options, debouncedSearch]);
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Skeleton width={20} />
|
||||
<Skeleton width={100} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectedItem = () => {
|
||||
return (
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
{selected?.icon && <selected.icon size={14} width={14} />}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col text-sm font-medium",
|
||||
size === "xs" && "text-xs",
|
||||
)}
|
||||
>
|
||||
<span className={"text-nb-gray-200"}>{selected?.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlaceholderItem = () => {
|
||||
return (
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col text-sm font-medium",
|
||||
size === "xs" && "text-xs",
|
||||
)}
|
||||
>
|
||||
<span className={"text-nb-gray-200"}>{placeholder}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
@@ -91,45 +139,26 @@ export function SelectDropdown({
|
||||
setOpen(isOpen);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild={true} disabled={disabled || isLoading}>
|
||||
<Button
|
||||
variant={variant}
|
||||
disabled={disabled || isLoading}
|
||||
ref={inputRef}
|
||||
className={"w-full"}
|
||||
>
|
||||
<div className={"w-full flex justify-between items-center gap-2"}>
|
||||
{isLoading ? (
|
||||
<div className={"flex gap-2"}>
|
||||
<Skeleton width={20} />
|
||||
<Skeleton width={100} />
|
||||
<PopoverTrigger asChild={!children} disabled={disabled || isLoading}>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<Button
|
||||
variant={variant}
|
||||
disabled={disabled || isLoading}
|
||||
ref={inputRef}
|
||||
className={cn("w-full", className)}
|
||||
>
|
||||
<div className={"w-full flex justify-between items-center gap-2"}>
|
||||
{isLoading && <Loading />}
|
||||
{!isLoading && selected && <SelectedItem />}
|
||||
{!isLoading && !selected && <PlaceholderItem />}
|
||||
<div className={"pl-2"}>
|
||||
<ChevronsUpDown size={18} className={"shrink-0"} />
|
||||
</div>
|
||||
) : selected ? (
|
||||
<React.Fragment>
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
{selected?.icon && <selected.icon size={14} width={14} />}
|
||||
<div className={"flex flex-col text-sm font-medium"}>
|
||||
<span className={"text-nb-gray-200"}>
|
||||
{selected?.label}
|
||||
</span>
|
||||
</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"}>
|
||||
<ChevronsUpDown size={18} className={"shrink-0"} />
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-full p-0 shadow-sm shadow-nb-gray-950 focus:outline-none"
|
||||
@@ -164,18 +193,22 @@ export function SelectDropdown({
|
||||
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
"max-h-[380px] overflow-y-auto flex flex-col gap-1 pl-2 pb-2 pr-3",
|
||||
"overflow-y-auto flex flex-col gap-1 pl-2 pr-3",
|
||||
!showSearch && "pt-2",
|
||||
)}
|
||||
style={{
|
||||
maxHeight: maxHeight ?? 380,
|
||||
}}
|
||||
>
|
||||
<CommandGroup>
|
||||
<div className={"grid grid-cols-1 gap-1"}>
|
||||
<div className={"grid grid-cols-1 gap-1 pb-2"}>
|
||||
{filteredItems.map((option) => (
|
||||
<SelectDropdownItem
|
||||
option={option}
|
||||
toggle={toggle}
|
||||
key={option.value}
|
||||
showValue={showValues}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -192,10 +225,12 @@ const SelectDropdownItem = ({
|
||||
option,
|
||||
toggle,
|
||||
showValue = false,
|
||||
size = "sm",
|
||||
}: {
|
||||
option: SelectOption;
|
||||
toggle: (value: string) => void;
|
||||
showValue?: boolean;
|
||||
size: "xs" | "sm";
|
||||
}) => {
|
||||
const value = option.value || "" + option.label || "";
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
@@ -221,13 +256,20 @@ const SelectDropdownItem = ({
|
||||
>
|
||||
<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"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col text-sm font-medium",
|
||||
size === "xs" && "text-xs",
|
||||
)}
|
||||
>
|
||||
<span className={"text-nb-gray-200"}>{option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
{showValue && (
|
||||
<div className={"flex items-center gap-2.5 p-1"}>
|
||||
<Paragraph className={cn("text-sm text-right")}>
|
||||
<Paragraph
|
||||
className={cn("text-sm text-right", size === "xs" && "text-xs")}
|
||||
>
|
||||
{option.value}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -38,15 +37,9 @@ export const SelectDropdownSearchInput = forwardRef<HTMLInputElement, Props>(
|
||||
<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
|
||||
className={"absolute right-0 top-0 h-full flex items-center pr-4"}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ type Props = {
|
||||
description?: string;
|
||||
button?: React.ReactNode;
|
||||
learnMore?: React.ReactNode;
|
||||
showBackground?: boolean;
|
||||
};
|
||||
|
||||
export default function GetStartedTest({
|
||||
@@ -18,28 +19,33 @@ export default function GetStartedTest({
|
||||
description,
|
||||
button,
|
||||
learnMore,
|
||||
showBackground = true,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={"px-8 mt-8"}>
|
||||
<Card className={"w-full relative overflow-hidden"}>
|
||||
<div
|
||||
className={
|
||||
"absolute z-20 bg-gradient-to-b dark:to-nb-gray-950 dark:from-nb-gray-950/40 w-full h-full"
|
||||
}
|
||||
></div>
|
||||
<div
|
||||
className={
|
||||
"absolute w-full h-full left-0 top-0 z-10 px-5 py-3 overflow-hidden"
|
||||
}
|
||||
>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
</div>
|
||||
</div>
|
||||
{showBackground && (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
"absolute z-20 bg-gradient-to-b dark:to-nb-gray-950 dark:from-nb-gray-950/40 w-full h-full"
|
||||
}
|
||||
></div>
|
||||
<div
|
||||
className={
|
||||
"absolute w-full h-full left-0 top-0 z-10 px-5 py-3 overflow-hidden"
|
||||
}
|
||||
>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
<Skeleton className={"w-full"} height={70} duration={4} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={"w-full h-full z-20 relative left-0 top-0 flex py-8"}>
|
||||
<div className={"inline-flex justify-center w-full"}>
|
||||
<div>
|
||||
|
||||
@@ -11,9 +11,11 @@ import { useGroupIdentification } from "@/modules/groups/useGroupIdentification"
|
||||
export const GroupBadgeIcon = ({
|
||||
id,
|
||||
issued,
|
||||
size = 12,
|
||||
}: {
|
||||
id?: string;
|
||||
issued?: GroupIssued;
|
||||
size?: number;
|
||||
}) => {
|
||||
const { groups } = useGroups();
|
||||
const group = groups?.find((g) => g.id === id);
|
||||
@@ -22,11 +24,12 @@ export const GroupBadgeIcon = ({
|
||||
useGroupIdentification({ id, issued: issued ?? group?.issued });
|
||||
|
||||
if (isGoogleGroup)
|
||||
return <GoogleIcon size={11} className={"shrink-0 mr-0.5"} />;
|
||||
return <GoogleIcon size={size - 1} className={"shrink-0 mr-0.5"} />;
|
||||
if (isAzureGroup)
|
||||
return <EntraIcon size={13} className={"shrink-0 mr-0.5"} />;
|
||||
if (isOktaGroup) return <OktaIcon size={11} className={"shrink-0 mr-0.5"} />;
|
||||
if (isJWTGroup) return <JWTIcon size={12} className={"shrink-0"} />;
|
||||
return <EntraIcon size={size + 1} className={"shrink-0 mr-0.5"} />;
|
||||
if (isOktaGroup)
|
||||
return <OktaIcon size={size - 1} className={"shrink-0 mr-0.5"} />;
|
||||
if (isJWTGroup) return <JWTIcon size={size} className={"shrink-0"} />;
|
||||
|
||||
return <FolderGit2 size={12} className={"shrink-0"} />;
|
||||
return <FolderGit2 size={size} className={"shrink-0"} />;
|
||||
};
|
||||
@@ -62,7 +62,6 @@ const UserProfileProvider = ({ children }: Props) => {
|
||||
}
|
||||
}, [user, error, users, isLoading, isAllUsersLoading]);
|
||||
|
||||
|
||||
const data = useMemo(() => {
|
||||
return {
|
||||
loggedInUser,
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function useOperatingSystem() {
|
||||
* Falls back to Linux if the operating system is not recognized
|
||||
*/
|
||||
export const getOperatingSystem = (os: string) => {
|
||||
if (!os) return OperatingSystem.LINUX as const;
|
||||
if (os.toLowerCase().includes("freebsd"))
|
||||
return OperatingSystem.FREEBSD as const;
|
||||
if (os.toLowerCase().includes("darwin"))
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Peer {
|
||||
name: string;
|
||||
ip: string;
|
||||
connected: boolean;
|
||||
created_at?: Date;
|
||||
last_seen: Date;
|
||||
os: string;
|
||||
version: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from "@components/Button";
|
||||
import ButtonGroup from "@components/ButtonGroup";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
@@ -29,7 +30,6 @@ import AccessControlPortsCell from "@/modules/access-control/table/AccessControl
|
||||
import AccessControlPostureCheckCell from "@/modules/access-control/table/AccessControlPostureCheckCell";
|
||||
import AccessControlProtocolCell from "@/modules/access-control/table/AccessControlProtocolCell";
|
||||
import AccessControlSourcesCell from "@/modules/access-control/table/AccessControlSourcesCell";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
|
||||
type Props = {
|
||||
policies?: Policy[];
|
||||
@@ -201,40 +201,40 @@ export default function AccessControlTable({
|
||||
const [currentRow, setCurrentRow] = useState<Policy>();
|
||||
const [currentCellClicked, setCurrentCellClicked] = useState("");
|
||||
|
||||
const [showTemporaryPolicies, setShowTemporaryPolicies] = useState(false);
|
||||
const [showTemporaryPolicies, setShowTemporaryPolicies] = useState(false);
|
||||
|
||||
const withTemporaryPolicies = useCallback(
|
||||
(condition: boolean) =>
|
||||
policies?.filter((policy) =>
|
||||
condition
|
||||
? policy?.name?.startsWith("Temporary") &&
|
||||
policy?.name?.endsWith("client") &&
|
||||
policy?.description?.startsWith("Temporary") &&
|
||||
policy?.description?.endsWith("client")
|
||||
: !(
|
||||
policy?.name?.startsWith("Temporary") &&
|
||||
policy?.name?.endsWith("client") &&
|
||||
policy?.description?.startsWith("Temporary") &&
|
||||
policy?.description?.endsWith("client")
|
||||
),
|
||||
) ?? [],
|
||||
[policies],
|
||||
);
|
||||
const withTemporaryPolicies = useCallback(
|
||||
(condition: boolean) =>
|
||||
policies?.filter((policy) =>
|
||||
condition
|
||||
? policy?.name?.startsWith("Temporary") &&
|
||||
policy?.name?.endsWith("client") &&
|
||||
policy?.description?.startsWith("Temporary") &&
|
||||
policy?.description?.endsWith("client")
|
||||
: !(
|
||||
policy?.name?.startsWith("Temporary") &&
|
||||
policy?.name?.endsWith("client") &&
|
||||
policy?.description?.startsWith("Temporary") &&
|
||||
policy?.description?.endsWith("client")
|
||||
),
|
||||
) ?? [],
|
||||
[policies],
|
||||
);
|
||||
|
||||
const tempPolicies = useMemo(
|
||||
() => withTemporaryPolicies(true),
|
||||
[withTemporaryPolicies],
|
||||
);
|
||||
const regularPolicies = useMemo(
|
||||
() => withTemporaryPolicies(false),
|
||||
[withTemporaryPolicies],
|
||||
);
|
||||
const tempPolicies = useMemo(
|
||||
() => withTemporaryPolicies(true),
|
||||
[withTemporaryPolicies],
|
||||
);
|
||||
const regularPolicies = useMemo(
|
||||
() => withTemporaryPolicies(false),
|
||||
[withTemporaryPolicies],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (showTemporaryPolicies && tempPolicies?.length === 0) {
|
||||
setShowTemporaryPolicies(false);
|
||||
}
|
||||
}, [showTemporaryPolicies, tempPolicies]);
|
||||
useEffect(() => {
|
||||
if (showTemporaryPolicies && tempPolicies?.length === 0) {
|
||||
setShowTemporaryPolicies(false);
|
||||
}
|
||||
}, [showTemporaryPolicies, tempPolicies]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -338,91 +338,91 @@ export default function AccessControlTable({
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{(table) => {
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup disabled={policies?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(undefined);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === undefined
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(true);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Active
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(false);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === false
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Inactive
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={policies?.length == 0}
|
||||
/>
|
||||
{(table) => {
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup disabled={policies?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(undefined);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === undefined
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(true);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Active
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.getColumn("enabled")?.setFilterValue(false);
|
||||
}}
|
||||
disabled={policies?.length == 0}
|
||||
variant={
|
||||
table.getColumn("enabled")?.getFilterValue() === false
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Inactive
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={policies?.length == 0}
|
||||
/>
|
||||
|
||||
{tempPolicies?.length > 0 && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-sm text-xs"}>
|
||||
Show temporary policies created by the NetBird browser
|
||||
client. These policies are ephemeral and will be
|
||||
deleted automatically after a short period of time.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={"h-[44px]"}
|
||||
variant={showTemporaryPolicies ? "tertiary" : "secondary"}
|
||||
onClick={() => {
|
||||
setShowTemporaryPolicies(!showTemporaryPolicies);
|
||||
}}
|
||||
>
|
||||
<ClockFadingIcon size={16} />
|
||||
</Button>
|
||||
</FullTooltip>
|
||||
)}
|
||||
{tempPolicies?.length > 0 && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-sm text-xs"}>
|
||||
Show temporary policies created by the NetBird browser
|
||||
client. These policies are ephemeral and will be deleted
|
||||
automatically after a short period of time.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={"h-[44px]"}
|
||||
variant={showTemporaryPolicies ? "tertiary" : "secondary"}
|
||||
onClick={() => {
|
||||
setShowTemporaryPolicies(!showTemporaryPolicies);
|
||||
}}
|
||||
>
|
||||
<ClockFadingIcon size={16} />
|
||||
</Button>
|
||||
</FullTooltip>
|
||||
)}
|
||||
|
||||
<DataTableRefreshButton
|
||||
isDisabled={policies?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/policies").then();
|
||||
mutate("/groups").then();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
<DataTableRefreshButton
|
||||
isDisabled={policies?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/policies").then();
|
||||
mutate("/groups").then();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DataTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,9 +260,9 @@ export const useAccessControl = ({
|
||||
updatePolicy(
|
||||
policy,
|
||||
policyObj,
|
||||
() => {
|
||||
(p) => {
|
||||
mutate("/policies");
|
||||
onSuccess && onSuccess(policy);
|
||||
onSuccess && onSuccess(p);
|
||||
},
|
||||
"The policy was successfully saved",
|
||||
);
|
||||
|
||||
@@ -677,6 +677,20 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "account.settings.extra.flow.group.remove")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Limit traffic event group <Value>{m.group_name}</Value> removed
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "account.settings.extra.flow.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Limit traffic event group <Value>{m.group_name}</Value> added
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2.5 items-center"}>
|
||||
<span className={"mb-[1px]"}>{event.activity}</span>
|
||||
|
||||
Loaded 20 of 31 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user