mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-13 04:09:56 +00:00
update height and wording
This commit is contained in:
@@ -4,14 +4,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { Command } from "cmdk";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import {
|
||||
ChevronDown,
|
||||
MoreVertical,
|
||||
PlusCircle,
|
||||
Search,
|
||||
Trash2,
|
||||
UserMinus,
|
||||
} from "lucide-react";
|
||||
import { ChevronDown, MoreVertical, PlusCircle, Search, Trash2, UserMinus } from "lucide-react";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { generateColorFromString } from "@/lib/color";
|
||||
import { NewProfileDialog } from "@/components/NewProfileDialog";
|
||||
@@ -56,9 +49,7 @@ export const ProfileSelector = ({ email = "" }: Props) => {
|
||||
|
||||
const selected = profiles.find((p) => p.id === selectedId) ?? profiles[0];
|
||||
|
||||
const sorted = [...profiles].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
const sorted = [...profiles].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
setSelectedId(id);
|
||||
@@ -115,172 +106,145 @@ export const ProfileSelector = ({ email = "" }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
"h-11 rounded-md text-nb-gray-300 flex items-center gap-1 text-xs hover:bg-nb-gray-930 data-[state=open]:bg-nb-gray-930 px-2 -mx-1 outline-none cursor-default transition-colors duration-150"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold",
|
||||
email ? "h-7 w-7" : "h-6 w-6",
|
||||
)}
|
||||
style={{ color: initialColor }}
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
"h-11 rounded-md text-nb-gray-300 flex items-center gap-1 text-xs hover:bg-nb-gray-930 data-[state=open]:bg-nb-gray-930 px-2 -mx-1 outline-none cursor-default transition-colors duration-150"
|
||||
}
|
||||
>
|
||||
{initial}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap flex flex-col ml-1 text-left",
|
||||
email ? "mt-1" : "justify-center",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"leading-none text-nb-gray-200 font-semibold"
|
||||
}
|
||||
>
|
||||
{selected?.name ?? "No profile"}
|
||||
</span>
|
||||
{email && (
|
||||
<span
|
||||
className={
|
||||
"text-[0.73rem] font-normal text-nb-gray-300"
|
||||
}
|
||||
>
|
||||
{email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown size={14} className={"ml-2 mr-2"} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
className={cn(
|
||||
"w-72 rounded-md border border-nb-gray-920 bg-nb-gray-935 shadow-lg",
|
||||
"p-1 z-50 origin-[var(--radix-popover-content-transform-origin)]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-1",
|
||||
"data-[side=top]:slide-in-from-bottom-1",
|
||||
"duration-150 ease-out",
|
||||
)}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command
|
||||
loop
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
"[&_[cmdk-input-wrapper]]:flex [&_[cmdk-input-wrapper]]:items-center",
|
||||
)}
|
||||
>
|
||||
<div className="px-1 pb-1">
|
||||
<div className="group flex items-center gap-2 px-2 h-8">
|
||||
<Search
|
||||
size={12}
|
||||
className="text-nb-gray-300 shrink-0"
|
||||
/>
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder="Search profile by name..."
|
||||
className={cn(
|
||||
"w-full bg-transparent text-xs text-nb-gray-200 placeholder:text-nb-gray-400",
|
||||
"outline-none border-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea.Root
|
||||
type="auto"
|
||||
className="overflow-hidden -mx-1"
|
||||
>
|
||||
<ScrollArea.Viewport className="max-h-64 px-1 pb-1">
|
||||
<Command.List>
|
||||
<Command.Empty>
|
||||
<div className="flex flex-col items-center text-center px-4 pt-2 pb-3">
|
||||
<h3 className="text-xs font-semibold text-nb-gray-200">
|
||||
No Profiles Found
|
||||
</h3>
|
||||
<p className="text-[0.7rem] leading-snug text-nb-gray-400 mt-1 text-balance">
|
||||
Try a different search term
|
||||
or create a new profile.
|
||||
</p>
|
||||
</div>
|
||||
</Command.Empty>
|
||||
|
||||
{sorted.map((profile) => (
|
||||
<ProfileRow
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
selected={
|
||||
profile.id === selectedId
|
||||
}
|
||||
onSelect={() =>
|
||||
handleSelect(profile.id)
|
||||
}
|
||||
onDeregister={() =>
|
||||
handleDeregister(profile.id)
|
||||
}
|
||||
onDelete={() =>
|
||||
handleDelete(profile.id)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Command.List>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation="vertical"
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb className="flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
|
||||
<div className="h-px bg-nb-gray-920 -mx-1 my-1" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewProfile}
|
||||
<div
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-md cursor-default outline-none",
|
||||
"text-netbird hover:bg-nb-gray-920",
|
||||
"flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold",
|
||||
email ? "h-7 w-7" : "h-6 w-6",
|
||||
)}
|
||||
style={{ color: initialColor }}
|
||||
>
|
||||
{initial}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap flex flex-col ml-1 text-left",
|
||||
email ? "mt-1" : "justify-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-6 flex items-center justify-center rounded-md bg-nb-gray-900 shrink-0"
|
||||
}
|
||||
>
|
||||
<PlusCircle
|
||||
size={12}
|
||||
className="text-netbird"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-semibold">
|
||||
New Profile
|
||||
<span className={"leading-none text-nb-gray-200 font-semibold"}>
|
||||
{selected?.name ?? "No profile"}
|
||||
</span>
|
||||
</button>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
<NewProfileDialog
|
||||
open={newOpen}
|
||||
onOpenChange={setNewOpen}
|
||||
onCreate={handleCreateProfile}
|
||||
/>
|
||||
{email && (
|
||||
<span className={"text-[0.73rem] font-normal text-nb-gray-300"}>
|
||||
{email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown size={14} className={"ml-2 mr-2"} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
align="end"
|
||||
sideOffset={6}
|
||||
className={cn(
|
||||
"w-72 rounded-md border border-nb-gray-900 bg-nb-gray-930 shadow-lg",
|
||||
"p-1 z-50 origin-[var(--radix-popover-content-transform-origin)]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-1",
|
||||
"data-[side=top]:slide-in-from-bottom-1",
|
||||
"duration-150 ease-out",
|
||||
)}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command
|
||||
loop
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
"[&_[cmdk-input-wrapper]]:flex [&_[cmdk-input-wrapper]]:items-center",
|
||||
)}
|
||||
>
|
||||
<div className="px-1 pb-1">
|
||||
<div className="group flex items-center gap-2 px-2 h-8">
|
||||
<Search size={12} className="text-nb-gray-300 shrink-0" />
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder="Search profile by name..."
|
||||
className={cn(
|
||||
"w-full bg-transparent text-xs text-nb-gray-200 placeholder:text-nb-gray-400",
|
||||
"outline-none border-none",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea.Root type="auto" className="overflow-hidden -mx-1">
|
||||
<ScrollArea.Viewport className="max-h-64 px-1 pb-1">
|
||||
<Command.List>
|
||||
<Command.Empty>
|
||||
<div className="flex flex-col items-center text-center px-4 pt-2 pb-3">
|
||||
<h3 className="text-xs font-semibold text-nb-gray-200">
|
||||
No Profiles Found
|
||||
</h3>
|
||||
<p className="text-[0.7rem] leading-snug text-nb-gray-400 mt-1 text-balance">
|
||||
Try a different search term or create a new
|
||||
profile.
|
||||
</p>
|
||||
</div>
|
||||
</Command.Empty>
|
||||
|
||||
{sorted.map((profile) => (
|
||||
<ProfileRow
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
selected={profile.id === selectedId}
|
||||
onSelect={() => handleSelect(profile.id)}
|
||||
onDeregister={() => handleDeregister(profile.id)}
|
||||
onDelete={() => handleDelete(profile.id)}
|
||||
/>
|
||||
))}
|
||||
</Command.List>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation="vertical"
|
||||
className={cn(
|
||||
"flex select-none touch-none transition-colors",
|
||||
"w-1.5 bg-transparent py-1",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.Thumb className="flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
|
||||
<div className="h-px bg-nb-gray-920 -mx-1 my-1" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewProfile}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-md cursor-default outline-none",
|
||||
"text-netbird hover:bg-nb-gray-910",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-6 flex items-center justify-center rounded-md bg-nb-gray-900 shrink-0"
|
||||
}
|
||||
>
|
||||
<PlusCircle size={12} className="text-netbird" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold">New Profile</span>
|
||||
</button>
|
||||
</Command>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
<NewProfileDialog
|
||||
open={newOpen}
|
||||
onOpenChange={setNewOpen}
|
||||
onCreate={handleCreateProfile}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -293,13 +257,7 @@ type ProfileRowProps = {
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const ProfileRow = ({
|
||||
profile,
|
||||
selected,
|
||||
onSelect,
|
||||
onDeregister,
|
||||
onDelete,
|
||||
}: ProfileRowProps) => {
|
||||
const ProfileRow = ({ profile, selected, onSelect, onDeregister, onDelete }: ProfileRowProps) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const initial = profile.name.charAt(0).toUpperCase();
|
||||
const initialColor = generateColorFromString(profile.name);
|
||||
@@ -311,13 +269,15 @@ const ProfileRow = ({
|
||||
onSelect={() => onSelect()}
|
||||
className={cn(
|
||||
"group flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-md cursor-default outline-none",
|
||||
"data-[selected=true]:bg-nb-gray-920",
|
||||
selected && "bg-nb-gray-920",
|
||||
"data-[selected=true]:bg-nb-gray-910",
|
||||
selected && "bg-nb-gray-910",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-6 w-6 flex items-center justify-center rounded-md text-[0.65rem] font-semibold shrink-0 bg-nb-gray-900",
|
||||
"group-data-[selected=true]:bg-nb-gray-850",
|
||||
selected && "bg-nb-gray-850",
|
||||
)}
|
||||
style={{ color: initialColor }}
|
||||
>
|
||||
@@ -326,19 +286,13 @@ const ProfileRow = ({
|
||||
<span
|
||||
className={cn(
|
||||
"flex-1 truncate text-xs",
|
||||
selected
|
||||
? "text-nb-gray-200 font-semibold"
|
||||
: "text-nb-gray-200",
|
||||
selected ? "text-nb-gray-200 font-semibold" : "text-nb-gray-200",
|
||||
)}
|
||||
>
|
||||
{profile.name}
|
||||
</span>
|
||||
|
||||
<DropdownMenu.Root
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
modal={false}
|
||||
>
|
||||
<DropdownMenu.Root open={menuOpen} onOpenChange={setMenuOpen} modal={false}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
@@ -350,8 +304,8 @@ const ProfileRow = ({
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className={cn(
|
||||
"h-6 w-6 flex items-center justify-center rounded text-nb-gray-400 cursor-default",
|
||||
"hover:bg-nb-gray-900 hover:text-nb-gray-200 outline-none",
|
||||
"data-[state=open]:bg-nb-gray-900 data-[state=open]:text-nb-gray-200",
|
||||
"hover:bg-nb-gray-800 hover:text-nb-gray-200 outline-none",
|
||||
"data-[state=open]:bg-nb-gray-800 data-[state=open]:text-nb-gray-200",
|
||||
)}
|
||||
aria-label="More options"
|
||||
>
|
||||
@@ -360,13 +314,13 @@ const ProfileRow = ({
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
side="right"
|
||||
align="start"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className={cn(
|
||||
"w-44 rounded-md border border-nb-gray-920 bg-nb-gray-935 shadow-lg p-1 z-50",
|
||||
"w-44 rounded-md border border-nb-gray-850 bg-nb-gray-910 shadow-lg p-1 z-50",
|
||||
)}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
@@ -376,14 +330,11 @@ const ProfileRow = ({
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none",
|
||||
"text-xs text-nb-gray-200 data-[highlighted]:bg-nb-gray-920",
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none font-medium",
|
||||
"text-xs text-nb-gray-200 data-[highlighted]:bg-nb-gray-850",
|
||||
)}
|
||||
>
|
||||
<UserMinus
|
||||
size={14}
|
||||
className="text-nb-gray-300"
|
||||
/>
|
||||
<UserMinus size={14} className="text-nb-gray-300" />
|
||||
<span>Deregister</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
@@ -393,8 +344,8 @@ const ProfileRow = ({
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none",
|
||||
"text-xs text-red-500 data-[highlighted]:bg-nb-gray-920",
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none font-medium",
|
||||
"text-xs text-red-500 data-[highlighted]:bg-nb-gray-850",
|
||||
)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SettingsSSH() {
|
||||
)}
|
||||
>
|
||||
<div className={"flex-1 max-w-md"}>
|
||||
<Label as={"div"}>JWT Cache TTL (Seconds)</Label>
|
||||
<Label as={"div"}>JWT Cache TTL</Label>
|
||||
<HelpText margin={false}>
|
||||
How long this client caches a JWT before prompting again on outgoing SSH
|
||||
connections. Set to 0 to disable caching and authenticate on every
|
||||
@@ -114,7 +114,7 @@ export function SettingsSSH() {
|
||||
value={jwtTtlInput}
|
||||
onChange={handleJwtTtlChange}
|
||||
onBlur={handleJwtTtlBlur}
|
||||
customSuffix={"Seconds"}
|
||||
customSuffix={"Second(s)"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function SettingsTroubleshooting() {
|
||||
onChange={(e) =>
|
||||
setTraceMinutes(Math.max(1, Math.min(30, Number(e.target.value) || 1)))
|
||||
}
|
||||
customSuffix={"Minutes"}
|
||||
customSuffix={"Minute(s)"}
|
||||
disabled={!trace}
|
||||
/>
|
||||
</div>
|
||||
@@ -119,12 +119,7 @@ function ProgressSection({ stage, onCancel }: { stage: DebugStage; onCancel: ()
|
||||
"Collecting logs, system details, and connection state. This usually takes a moment — keep this window open until it completes."
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
onClick={onCancel}
|
||||
disabled={cancelling}
|
||||
>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onCancel} disabled={cancelling}>
|
||||
{cancelling ? "Cancelling…" : "Cancel"}
|
||||
</Button>
|
||||
}
|
||||
@@ -186,20 +181,12 @@ function DoneResult({
|
||||
Close
|
||||
</Button>
|
||||
{showKey ? (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"xs"}
|
||||
copy={result.uploadedKey}
|
||||
>
|
||||
<Button variant={"primary"} size={"xs"} copy={result.uploadedKey}>
|
||||
Copy Key
|
||||
</Button>
|
||||
) : (
|
||||
result.path && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"xs"}
|
||||
onClick={onRevealPath}
|
||||
>
|
||||
<Button variant={"primary"} size={"xs"} onClick={onRevealPath}>
|
||||
<FolderOpen size={12} />
|
||||
Open Folder
|
||||
</Button>
|
||||
@@ -235,10 +222,8 @@ function DoneResult({
|
||||
}
|
||||
>
|
||||
Upload failed
|
||||
{result.uploadFailureReason
|
||||
? `: ${result.uploadFailureReason}`
|
||||
: "."}{" "}
|
||||
The bundle is still saved locally.
|
||||
{result.uploadFailureReason ? `: ${result.uploadFailureReason}` : "."} The
|
||||
bundle is still saved locally.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -104,8 +104,8 @@ func main() {
|
||||
Title: "NetBird",
|
||||
Width: 925,
|
||||
MinWidth: 925,
|
||||
Height: 600,
|
||||
MinHeight: 600,
|
||||
Height: 615,
|
||||
MinHeight: 615,
|
||||
Hidden: false,
|
||||
BackgroundColour: application.NewRGB(24, 26, 29),
|
||||
URL: "/",
|
||||
|
||||
Reference in New Issue
Block a user