Compare commits

..
Author SHA1 Message Date
Eduard Gert 41dfccca76 Add description for network resource 2025-01-22 12:12:09 +01:00
Eduard GertandMisha Bragin 25be69e7bb Add improvements to new networks features (#439)
* Fix wrong ui state for routing peer modal in networks

* Add confirmation dialog when blocking users

* Keep peer sort order when switching pages

* Update sidebar navigation order and remove deprecation notice

* Fix issue when hovering over truncated text in a group badge closes the multiple groups popover

* Update group text in network resource modal

* Update networks page text

* Fix line height

* Add search to resource table

* Switch networks flow to create first resources and then add routers

* Add enabled toggle to routing peers

* Add enabled toggle to network resources

* Add resource group modal and adjust tables

* Clarify networks

* Fix not properly aligned horizontal scroll bar

* Add option to install netbird after creating a setup key

* Fix text for install netbird modal

* Show resources count in group settings

* Fix "no results" and "no routing peers" text showing at the same time

* Fix wording

* Fix resource policy count

* Hide resource count when selection source groups

* Extend networks routing peer modal with option to create a setup key and install netbird

* Add option for horizontal stepper

* Generate setup key when installing netbird from routing peer modal

* Add confirm dialog to let the user know a one-off setup-key will be created. This avoids accidental clicking and later confusion on the setup keys page

---------

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
2025-01-20 16:18:21 +01:00
Eduard Gert 43e5d5cf53 Fix activity search and allow searching for meta fields (#440) 2025-01-15 16:41:55 +01:00
Eduard Gert 18819d6fdf Add confirmation dialog when blocking users (#437) 2025-01-15 16:29:05 +01:00
Eduard Gert 158804c1ac Fix wrong ui state for routing peer modal in networks (#436) 2025-01-15 16:28:50 +01:00
Misha Bragin 14d2d68819 Update links to networks doc (#435) 2024-12-27 21:48:36 +01:00
Pascal Fischer 40902b3629 add resources to groups update operation (#434) 2024-12-27 14:20:11 +01:00
Pascal Fischer fa9bcea4ab Update links for networks concept (#433) 2024-12-23 18:08:06 +01:00
Eduard Gert 3ba7acdecf Add new networks feature (#427) 2024-12-23 13:20:01 +03:00
Eduard Gert c7775ade8c Hide groups for regular users (#423) 2024-11-20 17:50:28 +01:00
110 changed files with 5381 additions and 536 deletions

No files matched your search

+719 -195
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -19,6 +19,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -55,8 +56,8 @@
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.383.0",
"next": "13.5.5",
"lucide-react": "^0.460.0",
"next": "13.5.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18",
@@ -76,7 +77,7 @@
"typescript": "^5"
},
"devDependencies": {
"cypress": "^13.3.3",
"cypress": "^13.13.0",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
+22 -7
View File
@@ -14,17 +14,24 @@ import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import Skeleton from "react-loading-skeleton";
import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { NameserverSettings } from "@/interfaces/NameserverSettings";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
export default function NameServerSettings() {
const { data: settings, isLoading } =
useFetchApi<NameserverSettings>("/dns/settings");
const initialDNSGroups = useGroupIdsToGroups(
settings?.disabled_management_groups,
);
return (
<PageContainer>
<div className={"p-default py-6"}>
@@ -55,10 +62,16 @@ export default function NameServerSettings() {
in our documentation.
</Paragraph>
<RestrictedAccess page={"DNS Settings"}>
{!isLoading && (
<SettingDisabledManagementGroups
initial={settings?.disabled_management_groups}
/>
{!isLoading && initialDNSGroups !== undefined ? (
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
) : (
<div>
<Skeleton
width={"100%"}
className={"mt-8 max-w-xl"}
height={240}
/>
</div>
)}
</RestrictedAccess>
</div>
@@ -67,16 +80,16 @@ export default function NameServerSettings() {
}
const SettingDisabledManagementGroups = ({
initial,
initialGroups,
}: {
initial: string[] | undefined;
initialGroups: Group[];
}) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initial || [],
initial: initialGroups,
});
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
@@ -108,6 +121,7 @@ const SettingDisabledManagementGroups = ({
Peers in these groups will require manual domain name resolution
</HelpText>
<PeerGroupSelector
dataCy={"dns-groups-selector"}
onChange={setSelectedGroups}
values={selectedGroups}
/>
@@ -122,6 +136,7 @@ const SettingDisabledManagementGroups = ({
size={"sm"}
onClick={saveSettings}
disabled={!hasChanges}
data-cy={"save-changes"}
>
Save Changes
</Button>
+8
View 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: `Network - Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+229
View File
@@ -0,0 +1,229 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Card from "@components/Card";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import NetworkModal from "@/modules/networks/NetworkModal";
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
const networkId = queryParameter.get("id");
const { data: network, isLoading } = useFetchApi<Network>(
`/networks/${networkId}`,
true,
);
useRedirect("/networks", false, !networkId);
return network && !isLoading ? (
<NetworkOverview network={network} />
) : (
<FullScreenLoading />
);
}
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
const { isUser } = useLoggedInUser();
const [networkModal, setNetworkModal] = useState(false);
const { mutate } = useSWRConfig();
const isActive = !!(
network?.routing_peers_count && network.routing_peers_count > 0
);
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={isUser}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
<button
className={
"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 cursor-pointer"
}
onClick={() => setNetworkModal(true)}
>
<PencilLineIcon size={18} />
</button>
<NetworkModal
open={networkModal}
setOpen={setNetworkModal}
onUpdated={() => {
mutate(`/networks/${network.id}`);
}}
network={network}
/>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Separator />
<ResourcesSection network={network} />
<div className={"h-3"} />
<Separator />
<NetworkRoutingPeersSection network={network} />
</NetworkProvider>
</PageContainer>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
);
const disabledText = useMemo(
() => (
<>
High availability is currently{" "}
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
network.
</>
),
[],
);
const enabledText = useMemo(
() => (
<>
High availability is{" "}
<span className={"text-green-500 font-medium"}>active</span> for this
network.
</>
),
[],
);
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card.List>
<Card.ListItem
tooltip={false}
label={
<>
<ServerIcon size={16} />
High Availability
</>
}
value={
<FullTooltip
interactive={false}
content={
<div className={"max-w-xs text-xs"}>
{isHighlyAvailable ? enabledText : disabledText}
{isHighlyAvailable ? (
<div className={"inline-flex mt-2"}>
You can add more routing peers to increase the
availability of this network.
</div>
) : (
<div className={"inline-flex mt-2"}>
Go ahead and add more routing peers or groups with routing
peers to enable high availability for this network.
</div>
)}
</div>
}
>
<div
className={cn(
"flex gap-2.5 items-center text-nb-gray-300 text-sm cursor-help",
)}
>
<span
className={cn(
"h-2 w-2 rounded-full",
!isHighlyAvailable ? "bg-yellow-400" : "bg-green-500",
)}
></span>
{isHighlyAvailable ? "Active" : "Inactive"}
<HelpCircle size={12} />
</div>
</FullTooltip>
}
/>
<Card.ListItem
tooltip={false}
label={
policyCount > 0 ? (
<>
<ShieldCheckIcon size={16} className={"text-green-500"} />
{policyCount}{" "}
{policyCount === 1 ? "Active Policy" : "Active Policies"}
</>
) : (
<>
<ShieldXIcon size={16} className={"text-red-500"} />
No Active Policies
</>
)
}
value={
policyCount > 0 ? (
<InlineLink href={"/access-control"}>
Go to Policies
<ArrowUpRightIcon size={14} />
</InlineLink>
) : null
}
/>
</Card.List>
</Card>
);
}
+8
View 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: `Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+61
View File
@@ -0,0 +1,61 @@
"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 { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import NetworksTable from "@/modules/networks/table/NetworksTable";
export default function Networks() {
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access internal resources in LANs and VPCs without
installing NetBird on every machine.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/networks"}
target={"_blank"}
>
Networks
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess>
<Suspense fallback={<SkeletonTable />}>
<NetworksTable
data={networks}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+8 -25
View File
@@ -297,29 +297,12 @@ function PeerOverview() {
/>
</FullTooltip>
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<FullTooltip
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</div>
}
interactive={false}
className={"w-full block"}
disabled={!isUser}
>
{!isUser && (
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<PeerGroupSelector
disabled={isUser}
onChange={setSelectedGroups}
@@ -327,8 +310,8 @@ function PeerOverview() {
hideAllGroup={true}
peer={peer}
/>
</FullTooltip>
</div>
</div>
)}
</div>
</div>
</div>
+7
View File
@@ -6,6 +6,7 @@ import {
AlertOctagonIcon,
FolderGit2Icon,
LockIcon,
NetworkIcon,
ShieldIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
@@ -16,6 +17,7 @@ import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() {
@@ -47,6 +49,10 @@ export default function NetBirdSettings() {
<LockIcon size={14} />
Permissions
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="networks">
<NetworkIcon size={14} />
Networks
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} />
Danger zone
@@ -57,6 +63,7 @@ export default function NetBirdSettings() {
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />}
</div>
</RestrictedAccess>
+16 -5
View File
@@ -22,11 +22,13 @@ import { useSWRConfig } from "swr";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { Role, User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
@@ -45,8 +47,10 @@ export default function UserPage() {
useRedirect("/team/users", false, !userId);
return !isLoading && user ? (
<UserOverview user={user} />
const userGroups = useGroupIdsToGroups(user?.auto_groups);
return !isLoading && user && userGroups !== undefined ? (
<UserOverview user={user} initialGroups={userGroups} />
) : (
<FullScreenLoading />
);
@@ -54,16 +58,16 @@ export default function UserPage() {
type Props = {
user: User;
initialGroups: Group[];
};
function UserOverview({ user }: Props) {
function UserOverview({ user, initialGroups }: Readonly<Props>) {
const router = useRouter();
const userRequest = useApiCall<User>("/users");
const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
const initialGroups = user.auto_groups;
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initialGroups,
@@ -180,6 +184,7 @@ function UserOverview({ user }: Props) {
className={"w-full"}
disabled={!hasChanges}
onClick={save}
data-cy={"save-changes"}
>
Save Changes
</Button>
@@ -201,6 +206,7 @@ function UserOverview({ user }: Props) {
onChange={setSelectedGroups}
values={selectedGroups}
hideAllGroup={true}
dataCy={"user-group-selector"}
/>
</div>
)}
@@ -244,7 +250,10 @@ function UserOverview({ user }: Props) {
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button variant={"primary"}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
>
<IconCirclePlus size={16} />
Create Access Token
</Button>
@@ -293,6 +302,7 @@ function UserInformationCard({ user }: { user: User }) {
)}
<Card.ListItem
tooltip={false}
label={
<>
<GalleryHorizontalEnd size={16} />
@@ -306,6 +316,7 @@ function UserInformationCard({ user }: { user: User }) {
<>
{!user.is_current && user.role != Role.Owner && (
<Card.ListItem
tooltip={false}
label={
<>
<Ban size={16} />
+3 -1
View File
@@ -10,12 +10,14 @@ import useFetchApi from "@utils/api";
import { ExternalLinkIcon, User2 } from "lucide-react";
import React, { lazy, Suspense } from "react";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useGroups } from "@/contexts/GroupsProvider";
import { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
export default function TeamUsers() {
const { isLoading: isGroupsLoading } = useGroups();
const { data: users, isLoading } = useFetchApi<User[]>(
"/users?service_user=false",
);
@@ -60,7 +62,7 @@ export default function TeamUsers() {
<Suspense fallback={<SkeletonTable />}>
<UsersTable
users={users}
isLoading={isLoading}
isLoading={isLoading || isGroupsLoading}
headingTarget={portalTarget}
/>
</Suspense>
+9 -2
View File
@@ -15,7 +15,9 @@ export const OIDCError = () => {
const params = useSearchParams();
const errorParam = params.get("error");
const accessDenied = errorParam === "access_denied";
const invalidRequest = errorParam === "invalid_request";
const [title, setTitle] = useState(params.get("error_description"));
const errorDescription = params.get("error_description");
const { logout, login } = useOidc();
useEffect(() => {
@@ -72,9 +74,14 @@ export const OIDCError = () => {
</>
) : (
<>
<Paragraph className={"text-center mt-2"}>
<Paragraph className={"text-center mt-2 block"}>
There was an error logging you in. <br />
Error: {oidcUserLoadingState}
Error:{" "}
<span className={"inline capitalize"}>
{invalidRequest && errorDescription
? errorDescription
: oidcUserLoadingState}
</span>
</Paragraph>
<Button
variant={"primary"}
+5
View File
@@ -35,6 +35,11 @@ export const buttonVariants = cva(
"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",
],
secondaryLighter: [
"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/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60",
],
input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
+14 -3
View File
@@ -1,12 +1,15 @@
import FullTooltip from "@components/FullTooltip";
import Paragraph from "@components/Paragraph";
import { cn } from "@utils/helpers";
import { cva } from "class-variance-authority";
import { cva, VariantProps } from "class-variance-authority";
import { AlertCircle } from "lucide-react";
import * as React from "react";
type InputVariants = VariantProps<typeof inputVariants>;
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
extends React.InputHTMLAttributes<HTMLInputElement>,
InputVariants {
customPrefix?: React.ReactNode;
customSuffix?: React.ReactNode;
maxWidthClass?: string;
@@ -14,6 +17,7 @@ export interface InputProps
error?: string;
errorTooltip?: boolean;
errorTooltipPosition?: "top" | "top-right";
prefixClassName?: string;
}
const inputVariants = cva("", {
@@ -23,6 +27,10 @@ const inputVariants = cva("", {
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-nb-gray-700",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
],
darker: [
"dark:bg-nb-gray-920 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-300 dark:border-nb-gray-800",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
],
error: [
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500",
"ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10",
@@ -51,6 +59,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
error,
errorTooltip = false,
errorTooltipPosition = "top",
variant = "default",
prefixClassName,
...props
},
ref,
@@ -67,6 +77,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-20",
prefixClassName,
)}
>
{customPrefix}
@@ -87,7 +98,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
className={cn(
inputVariants({ variant: error ? "error" : "default" }),
inputVariants({ variant: error ? "error" : variant }),
"flex h-[42px] w-full rounded-md bg-white px-3 py-2 text-sm file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-20 ",
"file:border-0",
"focus-visible:ring-2 focus-visible:ring-offset-2",
+137 -7
View File
@@ -7,22 +7,31 @@ import { ScrollArea } from "@components/ScrollArea";
import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount";
import GroupBadge from "@components/ui/GroupBadge";
import GroupBadgeWithEditPeers from "@components/ui/GroupBadgeWithEditPeers";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
import { useSearch } from "@hooks/useSearch";
import useSortedDropdownOptions from "@hooks/useSortedDropdownOptions";
import { IconArrowBack } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { sortBy, trim, unionBy } from "lodash";
import {
ChevronsUpDown,
FolderGit2,
GlobeIcon,
Layers3,
MonitorSmartphoneIcon,
NetworkIcon,
SearchIcon,
WorkflowIcon,
} from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { useElementSize } from "@/hooks/useElementSize";
import type { Group, GroupPeer } from "@/interfaces/Group";
import type { Group, GroupPeer, GroupResource } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
interface MultiSelectProps {
@@ -39,6 +48,7 @@ interface MultiSelectProps {
showRoutes?: boolean;
disabledGroups?: Group[];
dataCy?: string;
showResourceCounter?: boolean;
}
export function PeerGroupSelector({
onChange,
@@ -54,6 +64,7 @@ export function PeerGroupSelector({
showRoutes = false,
disabledGroups,
dataCy = "group-selector-dropdown",
showResourceCounter = true,
}: Readonly<MultiSelectProps>) {
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
@@ -95,19 +106,35 @@ export function PeerGroupSelector({
const option = dropdownOptions.find((option) => option.name == name);
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
const groupResources: GroupResource[] | undefined =
(group?.resources as GroupResource[]) || [];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers }]);
addDropdownOptions([
{ name: name, peers: groupPeers, resources: groupResources },
]);
}
if (max == 1 && values.length == 1) {
onChange([{ name: name, id: group?.id, peers: groupPeers }]);
onChange([
{
name: name,
id: group?.id,
peers: groupPeers,
resources: groupResources,
},
]);
} else {
onChange((previous) => [
...previous,
{ name: name, id: group?.id, peers: groupPeers },
{
name: name,
id: group?.id,
peers: groupPeers,
resources: groupResources,
},
]);
}
@@ -241,7 +268,7 @@ export function PeerGroupSelector({
)}
</div>
<div className={"pl-2"}>
<div className={"pl-2"} data-cy={"group-selector-open-close"}>
<ChevronsUpDown
size={18}
className={"shrink-0 group-hover:text-nb-gray-300 transition-all"}
@@ -311,7 +338,10 @@ export function PeerGroupSelector({
<CommandGroup>
<ScrollArea
className={"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3"}
className={cn(
"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3",
sortedDropdownOptions.length == 0 && !search && "py-0",
)}
>
{searchedGroupNotFound && (
<CommandItem
@@ -382,6 +412,10 @@ export function PeerGroupSelector({
<AccessControlGroupCount group_id={option.id} />
)}
{showResourceCounter && (
<ResourcesCounter group={option} />
)}
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
@@ -404,3 +438,99 @@ export function PeerGroupSelector({
</Popover>
);
}
const ResourcesCounter = ({ group }: { group: Group }) => {
return group?.resources_count && group.resources_count > 0 ? (
<div
className={
"text-nb-gray-300 font-medium flex items-center gap-2 transition-all"
}
>
<Layers3 size={14} className={"shrink-0"} />
{group.resources_count} Resource(s)
</div>
) : null;
};
const resourcesSearchPredicate = (item: NetworkResource, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item.address.toLowerCase().includes(lowerCaseQuery);
};
const ResourcesList = ({ search }: { search: string }) => {
const { data: resources, isLoading } = useFetchApi<NetworkResource[]>(
"/networks/resources",
);
const [filteredItems, _, setSearch] = useSearch(
resources || [],
resourcesSearchPredicate,
{ filter: true, debounce: 150 },
);
useEffect(() => {
setSearch(search);
}, [search, setSearch]);
return isLoading ? (
<>Loading...</>
) : (
filteredItems.length > 0 && (
<VirtualScrollAreaList
items={filteredItems}
onSelect={(option) => null}
renderItem={(res) => {
const isSelected = false;
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")}
onClick={(e) => {
e.preventDefault();
}}
>
{res.type === "host" && (
<WorkflowIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "domain" && (
<GlobeIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "subnet" && (
<NetworkIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
<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"
}
>
<Checkbox checked={isSelected} />
</div>
</div>
</Fragment>
);
}}
/>
)
);
};
+9 -7
View File
@@ -166,15 +166,17 @@ export function PeerSelector({
placeholder={"Search for peers by name or ip..."}
/>
{unfilteredItems.length == 0 && (
<DropdownInfoText>
{
"Seems like you don't have any linux peers to assign as a routing peer."
}
</DropdownInfoText>
{unfilteredItems.length == 0 && !search && (
<div className={"max-w-xs mx-auto"}>
<DropdownInfoText>
{
"Seems like you don't have any Linux peers to assign as a routing peer."
}
</DropdownInfoText>
</div>
)}
{filteredItems.length == 0 && (
{filteredItems.length == 0 && search != "" && (
<DropdownInfoText>
There are no peers matching your search.
</DropdownInfoText>
+16 -35
View File
@@ -1,5 +1,3 @@
"use client";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@utils/helpers";
import * as React from "react";
@@ -15,46 +13,31 @@ const ScrollArea = React.forwardRef<
>(({ className, children, withoutViewport = false, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn(
"relative will-change-scroll webkit-scroll",
className,
"overflow-hidden",
)}
className={cn("relative overflow-hidden", className)}
{...props}
>
{withoutViewport ? (
children
) : (
<ScrollAreaViewport disableOverflowY={false}>
{children}
</ScrollAreaViewport>
<ScrollAreaViewport>{children}</ScrollAreaViewport>
)}
<ScrollBar />
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
type AdditionalScrollAreaViewportProps = {
disableOverflowY?: boolean;
};
const ScrollAreaViewport = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> &
AdditionalScrollAreaViewportProps
>(({ disableOverflowY = true, ...props }, ref) => {
return (
<ScrollAreaPrimitive.Viewport
ref={ref}
className="h-full w-full rounded-[inherit] will-change-scroll webkit-scroll"
{...props}
style={
disableOverflowY ? { overflowY: undefined, ...props.style } : undefined
}
/>
);
});
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn("h-full w-full rounded-[inherit]", className)}
{...props}
/>
));
ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
const ScrollBar = React.forwardRef<
@@ -63,14 +46,11 @@ const ScrollBar = React.forwardRef<
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
style={{ boxSizing: "unset", overflow: undefined }}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
"flex select-none touch-none transition-colors",
orientation === "vertical" && "h-full w-2.5 p-[1px]",
orientation === "horizontal" && "w-full h-2.5 p-[1px] bottom-0",
className,
)}
{...props}
@@ -79,6 +59,7 @@ const ScrollBar = React.forwardRef<
className={cn(
"relative rounded-full bg-neutral-200 dark:bg-nb-gray-800",
orientation === "vertical" && "flex-1",
orientation === "horizontal" && "h-full",
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
+4 -2
View File
@@ -60,10 +60,12 @@ export default function SidebarItem({
<li className={"px-4 cursor-pointer"}>
<button
className={classNames(
"rounded-lg text-[.95rem] w-full ",
"rounded-lg text-[.87rem] w-full ",
"font-normal ",
className,
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",
isChild
? "pl-7 pr-2 py-[.45rem] mt-1 mb-0.5"
: "py-[.45rem] px-3",
isActive
? "text-gray-900 bg-gray-200 dark:text-white dark:bg-nb-gray-900"
: "text-gray-600 hover:bg-gray-200 dark:text-nb-gray-400 dark:hover:bg-nb-gray-900/50",
+27 -7
View File
@@ -4,9 +4,18 @@ import React from "react";
type Props = {
children: React.ReactNode;
className?: string;
horizontal?: boolean;
};
export default function Steps({ children, className }: Props) {
return <div className={cn("pt-4", className)}>{children}</div>;
export default function Steps({
children,
className,
horizontal = false,
}: Readonly<Props>) {
return (
<div className={cn("pt-4", horizontal && "flex", className)}>
{children}
</div>
);
}
type StepProps = {
@@ -14,21 +23,32 @@ type StepProps = {
step: number;
line?: boolean;
center?: boolean;
horizontal?: boolean;
};
const Step = ({ children, step, line = true, center = false }: StepProps) => {
const Step = ({
children,
step,
line = true,
center = false,
horizontal,
}: StepProps) => {
return (
<div
className={cn(
"flex gap-4 items-start min-w-full justify-start relative pb-6 -mx-1.5 group px-[2px]",
"flex gap-4 items-start justify-start relative pb-6 -mx-1.5 group px-[2px]",
center && "items-center",
horizontal ? "flex-col items-center" : "min-w-full",
)}
>
{line && (
<span
className={
"h-full w-[2px] bg-nb-gray-100 dark:bg-nb-gray-800 absolute left-0 ml-[18px] z-0 transition-all"
}
className={cn(
"bg-nb-gray-100 dark:bg-nb-gray-800 z-0 transition-all",
horizontal
? "w-full h-[2px] absolute mt-[16px] transform translate-x-1/2"
: "h-full w-[2px] absolute left-0 ml-[18px]",
)}
></span>
)}
+5 -2
View File
@@ -39,7 +39,7 @@ const Tabs = React.forwardRef<
Tabs.displayName = TabsPrimitive.Root.displayName;
type TabListProps = {
justify?: "start" | "end" | "center";
justify?: "start" | "end" | "center" | "between";
};
const TabsList = React.forwardRef<
@@ -54,6 +54,7 @@ const TabsList = React.forwardRef<
justify == "center" && "justify-center justify-items-end",
justify == "start" && "justify-start",
justify == "end" && "justify-end",
justify == "between" && "justify-between",
)}
{...props}
>
@@ -63,7 +64,9 @@ const TabsList = React.forwardRef<
}
/>
<ScrollArea>
<div className={"relative z-[1] flex flex-nowrap"}>{props.children}</div>
<div className={"relative z-[1] flex flex-nowrap w-full "}>
{props.children}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</TabsPrimitive.List>
Loaded 20 of 110 files, more files were not shown because too many files have changed in this diff. Show more