mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92528d8377 | ||
|
|
876f803033 | ||
|
|
43286352d1 |
No files matched your search
@@ -22,7 +22,6 @@ export interface Account {
|
||||
dns_domain: string;
|
||||
network_range?: string;
|
||||
lazy_connection_enabled: boolean;
|
||||
auto_update_version: string;
|
||||
};
|
||||
onboarding?: AccountOnboarding;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface User {
|
||||
pending_approval?: boolean;
|
||||
last_login?: Date;
|
||||
permissions: Permissions;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { notify } from "@components/Notification";
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import { useHasChanges } from "@hooks/useHasChanges";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { validator } from "@utils/helpers";
|
||||
import {
|
||||
ClockFadingIcon,
|
||||
ExternalLinkIcon,
|
||||
FlaskConicalIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -31,21 +20,6 @@ type Props = {
|
||||
account: Account;
|
||||
};
|
||||
|
||||
const latestOrCustomVersion = [
|
||||
{
|
||||
label: "Disabled",
|
||||
value: "disabled",
|
||||
},
|
||||
{
|
||||
label: "Latest Version",
|
||||
value: "latest",
|
||||
},
|
||||
{
|
||||
label: "Custom Version",
|
||||
value: "custom",
|
||||
},
|
||||
] as SelectOption[];
|
||||
|
||||
export default function ClientSettingsTab({ account }: Readonly<Props>) {
|
||||
const { permission } = usePermissions();
|
||||
|
||||
@@ -56,77 +30,6 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
|
||||
account.settings?.lazy_connection_enabled ?? false,
|
||||
);
|
||||
|
||||
const autoUpdateSetting = account.settings?.auto_update_version;
|
||||
const isAutoUpdateEnabled =
|
||||
!!autoUpdateSetting && autoUpdateSetting !== "disabled";
|
||||
const isCustomVersion = validator.isValidVersion(autoUpdateSetting);
|
||||
const [autoUpdateMethod, setAutoUpdateMethod] = useState(
|
||||
isAutoUpdateEnabled ? (isCustomVersion ? "custom" : "latest") : "disabled",
|
||||
);
|
||||
|
||||
const [autoUpdateCustomVersion, setAutoUpdateCustomVersion] = useState(
|
||||
isCustomVersion ? autoUpdateSetting : "",
|
||||
);
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
autoUpdateMethod,
|
||||
autoUpdateCustomVersion,
|
||||
]);
|
||||
|
||||
const handleUpdateMethodChange = (value: string) => {
|
||||
setAutoUpdateMethod(value);
|
||||
if (value === "disabled" || value === "latest") {
|
||||
setAutoUpdateCustomVersion("");
|
||||
}
|
||||
};
|
||||
|
||||
const versionError = useMemo(() => {
|
||||
const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
|
||||
if (autoUpdateCustomVersion == "") return "";
|
||||
if (autoUpdateCustomVersion == "-") return "";
|
||||
const validSemver = validator.isValidVersion(autoUpdateCustomVersion);
|
||||
if (!validSemver) return msg;
|
||||
return "";
|
||||
}, [autoUpdateCustomVersion]);
|
||||
|
||||
const canSaveCustomVersion =
|
||||
autoUpdateCustomVersion !== "" &&
|
||||
autoUpdateMethod === "custom" &&
|
||||
versionError === "";
|
||||
|
||||
const isSaveButtonDisabled = useMemo(() => {
|
||||
return (
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
(autoUpdateMethod === "custom" && !canSaveCustomVersion)
|
||||
);
|
||||
}, [
|
||||
hasChanges,
|
||||
permission.settings.update,
|
||||
autoUpdateMethod,
|
||||
canSaveCustomVersion,
|
||||
]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
notify({
|
||||
title: "Client Settings",
|
||||
description: `Client settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([autoUpdateMethod, autoUpdateCustomVersion]);
|
||||
}),
|
||||
loadingMessage: "Updating client settings...",
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLazyConnection = async (toggle: boolean) => {
|
||||
notify({
|
||||
title: "Lazy Connections",
|
||||
@@ -167,48 +70,10 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>Clients</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={isSaveButtonDisabled}
|
||||
onClick={saveChanges}
|
||||
data-cy={"save-clients-settings"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 w-full mt-8"}>
|
||||
<div className={"flex flex-col relative"}>
|
||||
<Label>
|
||||
<RefreshCcw size={15} />
|
||||
Automatic Updates
|
||||
</Label>
|
||||
<HelpText>
|
||||
Select how NetBird clients handle automatic updates by choosing
|
||||
the latest version, a custom version, or disabling updates
|
||||
altogether.
|
||||
</HelpText>
|
||||
<div className={"gap-4 items-center grid grid-cols-2"}>
|
||||
<SelectDropdown
|
||||
value={autoUpdateMethod}
|
||||
onChange={handleUpdateMethodChange}
|
||||
options={latestOrCustomVersion}
|
||||
/>
|
||||
<Input
|
||||
value={autoUpdateCustomVersion}
|
||||
customPrefix={"Version"}
|
||||
placeholder={"e.g., 0.52.2"}
|
||||
error={versionError}
|
||||
errorTooltip={true}
|
||||
disabled={autoUpdateMethod !== "custom"}
|
||||
onChange={(v) => {
|
||||
setAutoUpdateCustomVersion(v.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"mt-3"}>
|
||||
<div className={"mt-0"}>
|
||||
<h2 className={"text-lg font-medium"}>
|
||||
Experimental
|
||||
<FlaskConicalIcon
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Button from "@components/Button";
|
||||
import Code from "@components/Code";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
@@ -14,10 +15,11 @@ import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import { IconMailForward } from "@tabler/icons-react";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { MailIcon, User2 } from "lucide-react";
|
||||
import { CopyIcon, MailIcon, User2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import useCopyToClipboard from "@/hooks/useCopyToClipboard";
|
||||
import Avatar1 from "@/assets/avatars/009.jpg";
|
||||
import Avatar2 from "@/assets/avatars/030.jpg";
|
||||
import Avatar3 from "@/assets/avatars/063.jpg";
|
||||
@@ -26,33 +28,104 @@ import { Group } from "@/interfaces/Group";
|
||||
import { Role, User } from "@/interfaces/User";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
||||
import {isNetBirdHosted} from "@utils/netbird";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
groups?: Group[];
|
||||
};
|
||||
|
||||
const copyMessage = "Password was copied to your clipboard!";
|
||||
|
||||
export default function UserInviteModal({ children, groups }: Readonly<Props>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [successModal, setSuccessModal] = useState(false);
|
||||
const [createdUser, setCreatedUser] = useState<User>();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [, copyToClipboard] = useCopyToClipboard(createdUser?.password);
|
||||
|
||||
const handleOnSuccess = () => {
|
||||
setOpen(false);
|
||||
const handleOnSuccess = (user: User) => {
|
||||
if (user.password) {
|
||||
setCreatedUser(user);
|
||||
setSuccessModal(true);
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
setTimeout(() => {
|
||||
mutate("/users?service_user=false");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleCopyAndClose = () => {
|
||||
copyToClipboard(copyMessage).then(() => {
|
||||
setCreatedUser(undefined);
|
||||
setSuccessModal(false);
|
||||
setOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
|
||||
<ModalTrigger asChild={true}>{children}</ModalTrigger>
|
||||
<UserInviteModalContent onSuccess={handleOnSuccess} groups={groups} />
|
||||
</Modal>
|
||||
<>
|
||||
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
|
||||
<ModalTrigger asChild={true}>{children}</ModalTrigger>
|
||||
<UserInviteModalContent onSuccess={handleOnSuccess} groups={groups} />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={successModal}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCreatedUser(undefined);
|
||||
}
|
||||
setSuccessModal(open);
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
maxWidthClass={"max-w-md"}
|
||||
className={"mt-20"}
|
||||
showClose={false}
|
||||
>
|
||||
<div className={"pb-6 px-8"}>
|
||||
<div className={"flex flex-col items-center justify-center gap-3"}>
|
||||
<div>
|
||||
<h2 className={"text-2xl text-center mb-2"}>
|
||||
User created successfully!
|
||||
</h2>
|
||||
<Paragraph className={"mt-0 text-sm text-center"}>
|
||||
This password will not be shown again, so be sure to copy it
|
||||
and store in a secure location.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"px-8 pb-6"}>
|
||||
<Code message={copyMessage}>
|
||||
<Code.Line>{createdUser?.password || ""}</Code.Line>
|
||||
</Code>
|
||||
</div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={handleCopyAndClose}
|
||||
>
|
||||
<CopyIcon size={14} />
|
||||
Copy & Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ModalProps = {
|
||||
onSuccess: () => void;
|
||||
onSuccess: (user: User) => void;
|
||||
groups?: Group[];
|
||||
};
|
||||
|
||||
@@ -85,9 +158,9 @@ export function UserInviteModalContent({
|
||||
auto_groups: groupIds,
|
||||
is_service_user: false,
|
||||
})
|
||||
.then(() => {
|
||||
.then((user) => {
|
||||
mutate("/users?service_user=false");
|
||||
onSuccess && onSuccess();
|
||||
onSuccess && onSuccess(user);
|
||||
}),
|
||||
loadingMessage: "Sending invite...",
|
||||
});
|
||||
@@ -121,10 +194,10 @@ export function UserInviteModalContent({
|
||||
}
|
||||
>
|
||||
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
|
||||
Invite User
|
||||
{isNetBirdHosted() ? "Invite User" : "Create User"}
|
||||
</h2>
|
||||
<Paragraph className={cn("text-sm text-center max-w-xs")}>
|
||||
Invite a user to your network and set their permissions.
|
||||
{isNetBirdHosted() ? "Invite a user to your network and set their permissions." : "Create a NetBird user account with email and password."}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ export const InviteUserButton = ({
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
(isLocalDev() || isNetBirdHosted()) && (
|
||||
|
||||
<UserInviteModal groups={groups}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
@@ -285,9 +285,8 @@ export const InviteUserButton = ({
|
||||
disabled={!permission.users.create}
|
||||
>
|
||||
<MailPlus size={16} />
|
||||
Invite User
|
||||
{isNetBirdHosted() ? "Invite User" : "Create User"}
|
||||
</Button>
|
||||
</UserInviteModal>
|
||||
)
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user