mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Add lazy connection setting (#465)
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -53,7 +53,7 @@
|
|||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.479.0",
|
"lucide-react": "^0.481.0",
|
||||||
"next": "^14.2.28",
|
"next": "^14.2.28",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
@@ -6621,9 +6621,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.479.0",
|
"version": "0.481.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.479.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.481.0.tgz",
|
||||||
"integrity": "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==",
|
"integrity": "sha512-NrvUDNFwgLIvHiwTEq9boa5Kiz1KdUT8RJ+wmNijwxdn9U737Fw42c43sRxJTMqhL+ySHpGRVCWpwiF+abrEjw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.479.0",
|
"lucide-react": "^0.481.0",
|
||||||
"next": "^14.2.28",
|
"next": "^14.2.28",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
|
|||||||
@@ -157,20 +157,19 @@ const PeerGeneralInformation = () => {
|
|||||||
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
|
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
|
||||||
*/
|
*/
|
||||||
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
|
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
|
||||||
name,
|
|
||||||
ssh,
|
ssh,
|
||||||
selectedGroups,
|
selectedGroups,
|
||||||
loginExpiration,
|
loginExpiration,
|
||||||
inactivityExpiration,
|
inactivityExpiration,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updatePeer = async () => {
|
const updatePeer = async (newName?: string) => {
|
||||||
let batchCall: Promise<any>[] = [];
|
let batchCall: Promise<any>[] = [];
|
||||||
const groupCalls = getAllGroupCalls();
|
const groupCalls = getAllGroupCalls();
|
||||||
|
|
||||||
if (permission.peers.update) {
|
if (permission.peers.update) {
|
||||||
const updateRequest = update({
|
const updateRequest = update({
|
||||||
name,
|
name: newName ?? name,
|
||||||
ssh,
|
ssh,
|
||||||
loginExpiration,
|
loginExpiration,
|
||||||
inactivityExpiration,
|
inactivityExpiration,
|
||||||
@@ -187,7 +186,6 @@ const PeerGeneralInformation = () => {
|
|||||||
mutate("/peers/" + peer.id);
|
mutate("/peers/" + peer.id);
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
updateHasChangedRef([
|
updateHasChangedRef([
|
||||||
name,
|
|
||||||
ssh,
|
ssh,
|
||||||
selectedGroups,
|
selectedGroups,
|
||||||
loginExpiration,
|
loginExpiration,
|
||||||
@@ -229,8 +227,10 @@ const PeerGeneralInformation = () => {
|
|||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
<EditNameModal
|
<EditNameModal
|
||||||
onSuccess={(newName) => {
|
onSuccess={(newName) => {
|
||||||
setName(newName);
|
updatePeer(newName).then(() => {
|
||||||
setShowEditNameModal(false);
|
setName(newName);
|
||||||
|
setShowEditNameModal(false);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
initialName={name}
|
initialName={name}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
AlertOctagonIcon,
|
AlertOctagonIcon,
|
||||||
FolderGit2Icon,
|
FolderGit2Icon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
|
MonitorSmartphoneIcon,
|
||||||
NetworkIcon,
|
NetworkIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -16,6 +17,7 @@ import { useLoggedInUser } from "@/contexts/UsersProvider";
|
|||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import { useAccount } from "@/modules/account/useAccount";
|
import { useAccount } from "@/modules/account/useAccount";
|
||||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||||
|
import ClientSettingsTab from "@/modules/settings/ClientSettingsTab";
|
||||||
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||||
import GroupsTab from "@/modules/settings/GroupsTab";
|
import GroupsTab from "@/modules/settings/GroupsTab";
|
||||||
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
|
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
|
||||||
@@ -63,6 +65,10 @@ export default function NetBirdSettings() {
|
|||||||
<NetworkIcon size={14} />
|
<NetworkIcon size={14} />
|
||||||
Networks
|
Networks
|
||||||
</VerticalTabs.Trigger>
|
</VerticalTabs.Trigger>
|
||||||
|
<VerticalTabs.Trigger value="clients">
|
||||||
|
<MonitorSmartphoneIcon size={14} />
|
||||||
|
Clients
|
||||||
|
</VerticalTabs.Trigger>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -77,6 +83,7 @@ export default function NetBirdSettings() {
|
|||||||
{account && <PermissionsTab account={account} />}
|
{account && <PermissionsTab account={account} />}
|
||||||
{account && <GroupsTab account={account} />}
|
{account && <GroupsTab account={account} />}
|
||||||
{account && <NetworkSettingsTab account={account} />}
|
{account && <NetworkSettingsTab account={account} />}
|
||||||
|
{account && <ClientSettingsTab account={account} />}
|
||||||
{account && <DangerZoneTab account={account} />}
|
{account && <DangerZoneTab account={account} />}
|
||||||
</div>
|
</div>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement>, BadgeVariants {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
useHover?: boolean;
|
useHover?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variants = cva("", {
|
const variants = cva("", {
|
||||||
@@ -53,6 +54,7 @@ export default function Badge({
|
|||||||
className,
|
className,
|
||||||
variant = "blue",
|
variant = "blue",
|
||||||
useHover = false,
|
useHover = false,
|
||||||
|
disabled = false,
|
||||||
...props
|
...props
|
||||||
}: Readonly<Props>) {
|
}: Readonly<Props>) {
|
||||||
return (
|
return (
|
||||||
@@ -61,6 +63,7 @@ export default function Badge({
|
|||||||
"relative z-10 cursor-inherit whitespace-nowrap rounded-md text-[12px] py-1.5 px-3 font-normal flex gap-1.5 items-center justify-center transition-all",
|
"relative z-10 cursor-inherit whitespace-nowrap rounded-md text-[12px] py-1.5 px-3 font-normal flex gap-1.5 items-center justify-center transition-all",
|
||||||
className,
|
className,
|
||||||
variants({ variant, hover: useHover ? variant : "none" }),
|
variants({ variant, hover: useHover ? variant : "none" }),
|
||||||
|
disabled && "cursor-not-allowed opacity-50 select-none",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ type Props = {
|
|||||||
onError?: (error: boolean) => void;
|
onError?: (error: boolean) => void;
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
preventLeadingAndTrailingDots?: boolean;
|
||||||
|
allowWildcard?: boolean;
|
||||||
};
|
};
|
||||||
enum ActionType {
|
enum ActionType {
|
||||||
ADD = "ADD",
|
ADD = "ADD",
|
||||||
@@ -40,6 +42,8 @@ export default function InputDomain({
|
|||||||
onRemove,
|
onRemove,
|
||||||
onError,
|
onError,
|
||||||
disabled,
|
disabled,
|
||||||
|
preventLeadingAndTrailingDots,
|
||||||
|
allowWildcard = true,
|
||||||
}: Readonly<Props>) {
|
}: Readonly<Props>) {
|
||||||
const [name, setName] = useState(value?.name || "");
|
const [name, setName] = useState(value?.name || "");
|
||||||
|
|
||||||
@@ -52,7 +56,11 @@ export default function InputDomain({
|
|||||||
if (name == "") {
|
if (name == "") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const valid = validator.isValidDomain(name);
|
const valid = validator.isValidDomain(name, {
|
||||||
|
allowOnlyTld: true,
|
||||||
|
allowWildcard,
|
||||||
|
preventLeadingAndTrailingDots,
|
||||||
|
});
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ export const useRedirect = (
|
|||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If redirect is disabled or the url is already in the callback urls or the url is the current path then do not redirect
|
// If redirect is disabled or the url is already in the callback urls then do not redirect
|
||||||
if (!enable || callBackUrls.current.includes(url) || url === currentPath)
|
if (!enable || callBackUrls.current.includes(url)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const performRedirect = () => {
|
const performRedirect = () => {
|
||||||
if (!isRedirecting.current) {
|
if (!isRedirecting.current) {
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ export interface Account {
|
|||||||
regular_users_view_blocked: boolean;
|
regular_users_view_blocked: boolean;
|
||||||
routing_peer_dns_resolution_enabled: boolean;
|
routing_peer_dns_resolution_enabled: boolean;
|
||||||
dns_domain: string;
|
dns_domain: string;
|
||||||
|
lazy_connection_enabled: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import Badge from "@components/Badge";
|
|||||||
import { IconCirclePlus } from "@tabler/icons-react";
|
import { IconCirclePlus } from "@tabler/icons-react";
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { ShieldCheck } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
import { Policy } from "@/interfaces/Policy";
|
import { Policy } from "@/interfaces/Policy";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
policy: Policy;
|
policy: Policy;
|
||||||
};
|
};
|
||||||
export default function AccessControlPostureCheckCell({ policy }: Props) {
|
export default function AccessControlPostureCheckCell({ policy }: Props) {
|
||||||
|
const { permission } = usePermissions();
|
||||||
|
|
||||||
|
const isDisabled = !permission.policies.create || !permission.policies.update;
|
||||||
|
|
||||||
return policy.source_posture_checks &&
|
return policy.source_posture_checks &&
|
||||||
policy.source_posture_checks.length > 0 ? (
|
policy.source_posture_checks.length > 0 ? (
|
||||||
<div className={"flex"}>
|
<div className={"flex"}>
|
||||||
@@ -18,7 +23,14 @@ export default function AccessControlPostureCheckCell({ policy }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={"flex"}>
|
<div className={"flex"}>
|
||||||
<Badge variant={"gray"} useHover={true}>
|
<Badge
|
||||||
|
variant={"gray"}
|
||||||
|
useHover={!isDisabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (isDisabled) e.stopPropagation();
|
||||||
|
}}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
<IconCirclePlus size={14} />
|
<IconCirclePlus size={14} />
|
||||||
Add Posture Check
|
Add Posture Check
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
|||||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||||
import type { ColumnDef, SortingState } from "@tanstack/react-table";
|
import type { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||||
|
import { removeAllSpaces } from "@utils/helpers";
|
||||||
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
@@ -37,16 +38,20 @@ type Props = {
|
|||||||
|
|
||||||
export const AccessControlTableColumns: ColumnDef<Policy>[] = [
|
export const AccessControlTableColumns: ColumnDef<Policy>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
id: "name",
|
||||||
|
accessorFn: (row) => removeAllSpaces(row?.name),
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||||
},
|
},
|
||||||
sortingFn: "text",
|
sortingFn: "text",
|
||||||
|
filterFn: "fuzzy",
|
||||||
cell: ({ cell }) => <AccessControlNameCell policy={cell.row.original} />,
|
cell: ({ cell }) => <AccessControlNameCell policy={cell.row.original} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "description",
|
id: "description",
|
||||||
|
accessorFn: (row) => removeAllSpaces(row?.description),
|
||||||
sortingFn: "text",
|
sortingFn: "text",
|
||||||
|
filterFn: "fuzzy",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enabled",
|
id: "enabled",
|
||||||
|
|||||||
@@ -371,6 +371,8 @@ export function NameserverModalContent({
|
|||||||
{domains.map((domain, i) => {
|
{domains.map((domain, i) => {
|
||||||
return (
|
return (
|
||||||
<InputDomain
|
<InputDomain
|
||||||
|
preventLeadingAndTrailingDots={true}
|
||||||
|
allowWildcard={false}
|
||||||
key={domain.id}
|
key={domain.id}
|
||||||
value={domain}
|
value={domain}
|
||||||
onChange={(d) =>
|
onChange={(d) =>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApiCall } from "@utils/api";
|
import { useApiCall } from "@utils/api";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty, orderBy } from "lodash";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { useGroups } from "@/contexts/GroupsProvider";
|
import { useGroups } from "@/contexts/GroupsProvider";
|
||||||
@@ -20,13 +20,13 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
|
|||||||
const initialGroups = useMemo(() => {
|
const initialGroups = useMemo(() => {
|
||||||
if (!initial) return [];
|
if (!initial) return [];
|
||||||
const isArrayOfStrings = initial.every((item) => typeof item === "string");
|
const isArrayOfStrings = initial.every((item) => typeof item === "string");
|
||||||
if (!isArrayOfStrings) return initial as Group[];
|
if (!isArrayOfStrings) return orderBy(initial as Group[], ["name"]);
|
||||||
const foundGroups = initial
|
const foundGroups = initial
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
return groups?.find((g) => g.id === id);
|
return groups?.find((g) => g.id === id);
|
||||||
})
|
})
|
||||||
.filter((g) => g !== undefined) as Group[];
|
.filter((g) => g !== undefined) as Group[];
|
||||||
return foundGroups ?? [];
|
return orderBy(foundGroups, ["name"]) ?? [];
|
||||||
}, [groups, initial]);
|
}, [groups, initial]);
|
||||||
|
|
||||||
const [selectedGroups, setSelectedGroups] = useState<Group[]>(initialGroups);
|
const [selectedGroups, setSelectedGroups] = useState<Group[]>(initialGroups);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export function ResourceModalContent({
|
|||||||
<ResourceSingleAddressInput value={address} onChange={setAddress} />
|
<ResourceSingleAddressInput value={address} onChange={setAddress} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Assigned Groups</Label>
|
<Label>Destination Groups</Label>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
Add this resource to groups and use them as destinations when
|
Add this resource to groups and use them as destinations when
|
||||||
creating policies
|
creating policies
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ export default function PostureCheckTable({
|
|||||||
<Button
|
<Button
|
||||||
variant={"primary"}
|
variant={"primary"}
|
||||||
className={"ml-auto"}
|
className={"ml-auto"}
|
||||||
disabled={!permission.policies.create}
|
disabled={
|
||||||
|
!permission.policies.create || !permission.policies.update
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(undefined);
|
setCurrentRow(undefined);
|
||||||
setPostureCheckModal(true);
|
setPostureCheckModal(true);
|
||||||
@@ -176,6 +178,9 @@ export default function PostureCheckTable({
|
|||||||
<Button
|
<Button
|
||||||
variant={"primary"}
|
variant={"primary"}
|
||||||
className={"ml-auto"}
|
className={"ml-auto"}
|
||||||
|
disabled={
|
||||||
|
!permission.policies.create || !permission.policies.update
|
||||||
|
}
|
||||||
onClick={() => setPostureCheckModal(true)}
|
onClick={() => setPostureCheckModal(true)}
|
||||||
>
|
>
|
||||||
<IconCirclePlus size={16} />
|
<IconCirclePlus size={16} />
|
||||||
|
|||||||
119
src/modules/settings/ClientSettingsTab.tsx
Normal file
119
src/modules/settings/ClientSettingsTab.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import Breadcrumbs from "@components/Breadcrumbs";
|
||||||
|
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||||
|
import InlineLink from "@components/InlineLink";
|
||||||
|
import { notify } from "@components/Notification";
|
||||||
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
|
import { useApiCall } from "@utils/api";
|
||||||
|
import {
|
||||||
|
ClockFadingIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
FlaskConicalIcon,
|
||||||
|
MonitorSmartphoneIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useSWRConfig } from "swr";
|
||||||
|
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||||
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
|
import { Account } from "@/interfaces/Account";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
account: Account;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ClientSettingsTab({ account }: Readonly<Props>) {
|
||||||
|
const { permission } = usePermissions();
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
|
||||||
|
|
||||||
|
const [lazyConnection, setLazyConnection] = useState(
|
||||||
|
account.settings?.lazy_connection_enabled ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleLazyConnection = async (toggle: boolean) => {
|
||||||
|
notify({
|
||||||
|
title: "Lazy Connections",
|
||||||
|
description: `Lazy Connections successfully ${
|
||||||
|
toggle ? "enabled" : "disabled"
|
||||||
|
}.`,
|
||||||
|
promise: saveRequest
|
||||||
|
.put({
|
||||||
|
id: account.id,
|
||||||
|
settings: {
|
||||||
|
...account.settings,
|
||||||
|
lazy_connection_enabled: toggle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setLazyConnection(toggle);
|
||||||
|
mutate("/accounts");
|
||||||
|
}),
|
||||||
|
loadingMessage: "Updating Lazy Connections setting...",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.Content value={"clients"}>
|
||||||
|
<div className={"p-default py-6 max-w-xl"}>
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Breadcrumbs.Item
|
||||||
|
href={"/settings"}
|
||||||
|
label={"Settings"}
|
||||||
|
icon={<SettingsIcon size={13} />}
|
||||||
|
/>
|
||||||
|
<Breadcrumbs.Item
|
||||||
|
href={"/settings?tab=clients"}
|
||||||
|
label={"Clients"}
|
||||||
|
icon={<MonitorSmartphoneIcon size={14} />}
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
<div className={"flex items-start justify-between"}>
|
||||||
|
<h1>Clients</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"flex flex-col gap-6 w-full mt-8"}>
|
||||||
|
<div className={"mt-0"}>
|
||||||
|
<h2 className={"text-lg font-medium"}>
|
||||||
|
Experimental
|
||||||
|
<FlaskConicalIcon
|
||||||
|
size={16}
|
||||||
|
className={"inline ml-1.5 relative -top-[2px]"}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<div className={"text-sm text-gray-400"}>
|
||||||
|
Lazy connections are an experimental feature. Functionality and
|
||||||
|
behavior may evolve. Instead of maintaining always-on connections,
|
||||||
|
NetBird activates them on-demand based on activity or signaling.{" "}
|
||||||
|
<InlineLink
|
||||||
|
href={"https://docs.netbird.io/how-to/lazy-connection"}
|
||||||
|
target={"_blank"}
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLinkIcon size={12} />
|
||||||
|
</InlineLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FancyToggleSwitch
|
||||||
|
value={lazyConnection}
|
||||||
|
onChange={toggleLazyConnection}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<ClockFadingIcon size={15} />
|
||||||
|
Enable Lazy Connections
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<>
|
||||||
|
Allow to establish connections between peers only when required.
|
||||||
|
This requires NetBird client v0.45 or higher. Changes will only
|
||||||
|
take effect after restarting the clients.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
disabled={!permission.settings.update}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
|||||||
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
|
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
|
||||||
}
|
}
|
||||||
target={"_blank"}
|
target={"_blank"}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
<ExternalLinkIcon size={12} />
|
<ExternalLinkIcon size={12} />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import FullTooltip from "@components/FullTooltip";
|
|||||||
import { ScrollArea } from "@components/ScrollArea";
|
import { ScrollArea } from "@components/ScrollArea";
|
||||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||||
import { cn, generateColorFromString } from "@utils/helpers";
|
import { cn, generateColorFromString } from "@utils/helpers";
|
||||||
|
import { orderBy } from "lodash";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { User } from "@/interfaces/User";
|
import { User } from "@/interfaces/User";
|
||||||
import { SmallUserAvatar } from "@/modules/users/SmallUserAvatar";
|
import { SmallUserAvatar } from "@/modules/users/SmallUserAvatar";
|
||||||
@@ -19,7 +20,7 @@ export const HorizontalUsersStack = ({
|
|||||||
avatarClassName,
|
avatarClassName,
|
||||||
side = "top",
|
side = "top",
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
let usersToDisplay = users?.slice(0, max) || [];
|
let usersToDisplay = orderBy(users?.slice(0, max) || [], ["name"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullTooltip
|
<FullTooltip
|
||||||
@@ -33,7 +34,7 @@ export const HorizontalUsersStack = ({
|
|||||||
className={"flex flex-col gap-2.5"}
|
className={"flex flex-col gap-2.5"}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{users?.map((user, index) => (
|
{orderBy(users, ["name"])?.map((user, index) => (
|
||||||
<div
|
<div
|
||||||
className={"flex items-center gap-2 first:pt-2 last:pb-2 pr-6"}
|
className={"flex items-center gap-2 first:pt-2 last:pb-2 pr-6"}
|
||||||
key={user?.id || index}
|
key={user?.id || index}
|
||||||
@@ -92,7 +93,7 @@ export const HorizontalUsersStack = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2 text-xs ml-1.5 transition-colors",
|
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2 text-xs ml-1.5 transition-colors whitespace-nowrap",
|
||||||
users.length > 0 && "group-hover/user-stack:text-nb-gray-200 ",
|
users.length > 0 && "group-hover/user-stack:text-nb-gray-200 ",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface MultiSelectProps {
|
|||||||
hideOwner?: boolean;
|
hideOwner?: boolean;
|
||||||
currentUser?: User;
|
currentUser?: User;
|
||||||
customTrigger?: React.ReactNode;
|
customTrigger?: React.ReactNode;
|
||||||
|
side?: "top" | "bottom" | "left" | "right";
|
||||||
|
align?: "start" | "center" | "end";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserRoles = [
|
export const UserRoles = [
|
||||||
@@ -72,6 +74,8 @@ export function UserRoleSelector({
|
|||||||
hideOwner = false,
|
hideOwner = false,
|
||||||
currentUser,
|
currentUser,
|
||||||
customTrigger,
|
customTrigger,
|
||||||
|
side = "bottom",
|
||||||
|
align = "start",
|
||||||
}: Readonly<MultiSelectProps>) {
|
}: Readonly<MultiSelectProps>) {
|
||||||
const [inputRef, { width }] = useElementSize<
|
const [inputRef, { width }] = useElementSize<
|
||||||
HTMLButtonElement | HTMLDivElement
|
HTMLButtonElement | HTMLDivElement
|
||||||
@@ -124,13 +128,15 @@ export function UserRoleSelector({
|
|||||||
>
|
>
|
||||||
<PopoverTrigger asChild={true}>
|
<PopoverTrigger asChild={true}>
|
||||||
{customTrigger ? (
|
{customTrigger ? (
|
||||||
<div ref={inputRef}>{customTrigger}</div>
|
<div ref={inputRef} className={"group/user-role-selector"}>
|
||||||
|
{customTrigger}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant={"input"}
|
variant={"input"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={"w-full"} // [data-state] open
|
className={"w-full group/user-role-selector"}
|
||||||
data-cy={"user-role-selector"}
|
data-cy={"user-role-selector"}
|
||||||
>
|
>
|
||||||
<div className={"w-full flex justify-between items-center gap-2"}>
|
<div className={"w-full flex justify-between items-center gap-2"}>
|
||||||
@@ -157,8 +163,8 @@ export function UserRoleSelector({
|
|||||||
style={{
|
style={{
|
||||||
width: popoverWidth === "auto" ? width : popoverWidth,
|
width: popoverWidth === "auto" ? width : popoverWidth,
|
||||||
}}
|
}}
|
||||||
align="start"
|
align={align}
|
||||||
side={"bottom"}
|
side={side}
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function removeAllSpaces(str: string) {
|
|||||||
|
|
||||||
export const generateColorFromString = (str: string) => {
|
export const generateColorFromString = (str: string) => {
|
||||||
if (str.includes("System")) return "#808080";
|
if (str.includes("System")) return "#808080";
|
||||||
|
if (str.toLowerCase().startsWith("netbird")) return "#f68330";
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
str.split("").forEach((char) => {
|
str.split("").forEach((char) => {
|
||||||
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
||||||
@@ -59,22 +60,49 @@ export const sleep = (ms: number) => {
|
|||||||
export const validator = {
|
export const validator = {
|
||||||
isValidDomain: (
|
isValidDomain: (
|
||||||
domain: string,
|
domain: string,
|
||||||
options?: { allowWildcard?: boolean; allowOnlyTld?: boolean },
|
options?: {
|
||||||
|
allowWildcard?: boolean;
|
||||||
|
allowOnlyTld?: boolean;
|
||||||
|
preventLeadingAndTrailingDots?: boolean;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const { allowWildcard = true, allowOnlyTld = true } = options || {
|
const {
|
||||||
|
allowWildcard = true,
|
||||||
|
allowOnlyTld = true,
|
||||||
|
preventLeadingAndTrailingDots = false,
|
||||||
|
} = options || {
|
||||||
allowWildcard: true,
|
allowWildcard: true,
|
||||||
allowOnlyTld: true,
|
allowOnlyTld: true,
|
||||||
|
preventLeadingAndTrailingDots: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const includesAtLeastOneDot = domain.includes(".");
|
const includesAtLeastOneDot = allowOnlyTld ? true : domain.includes(".");
|
||||||
const hasWhitespace = domain.includes(" ");
|
const hasWhitespace = domain.includes(" ");
|
||||||
const domainRegex =
|
/**
|
||||||
/^(?!-)[a-z0-9\u00a1-\uffff-*]{0,63}(?<!-)(\.[a-z0-9\u00a1-\uffff-*]{0,63})*$/i;
|
* Do not start or end with hyphen
|
||||||
|
* Allow any Unicode character
|
||||||
|
* Allow any Unicode number
|
||||||
|
* Allow hyphen, dot and asterisks
|
||||||
|
*/
|
||||||
|
const domainRegex = /^(?!-)[\p{L}\p{N}.*-]+(?<!-)$/u;
|
||||||
const isValidUnicodeDomain = domainRegex.test(domain);
|
const isValidUnicodeDomain = domainRegex.test(domain);
|
||||||
|
|
||||||
|
if (
|
||||||
|
preventLeadingAndTrailingDots &&
|
||||||
|
(domain.startsWith(".") || domain.endsWith("."))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (domain.length < 1 || domain.length > 255) {
|
if (domain.length < 1 || domain.length > 255) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!allowWildcard && domain.includes("*")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!allowWildcard && domain.startsWith("*.")) {
|
if (!allowWildcard && domain.startsWith("*.")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user