mirror of
https://github.com/KLogicHQ/nats-console.git
synced 2026-10-13 00:01:36 +00:00
818fe0728c
Add ip allowlist support
204 lines
5.1 KiB
TypeScript
204 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface DropdownMenuProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface DropdownMenuContextValue {
|
|
open: boolean;
|
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null);
|
|
|
|
function useDropdownMenu() {
|
|
const context = React.useContext(DropdownMenuContext);
|
|
if (!context) {
|
|
throw new Error('DropdownMenu components must be used within a DropdownMenu');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function DropdownMenu({ children }: DropdownMenuProps) {
|
|
const [open, setOpen] = React.useState(false);
|
|
|
|
return (
|
|
<DropdownMenuContext.Provider value={{ open, setOpen }}>
|
|
<div className="relative inline-block">{children}</div>
|
|
</DropdownMenuContext.Provider>
|
|
);
|
|
}
|
|
|
|
interface DropdownMenuTriggerProps {
|
|
children: React.ReactNode;
|
|
asChild?: boolean;
|
|
}
|
|
|
|
export function DropdownMenuTrigger({ children, asChild }: DropdownMenuTriggerProps) {
|
|
const { open, setOpen } = useDropdownMenu();
|
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setOpen(!open);
|
|
};
|
|
|
|
if (asChild && React.isValidElement(children)) {
|
|
return React.cloneElement(children as React.ReactElement<any>, {
|
|
onClick: (e: React.MouseEvent) => {
|
|
handleClick(e);
|
|
const originalOnClick = (children as React.ReactElement<any>).props.onClick;
|
|
if (originalOnClick) originalOnClick(e);
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<button type="button" onClick={handleClick}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
align?: 'start' | 'center' | 'end';
|
|
sideOffset?: number;
|
|
}
|
|
|
|
export function DropdownMenuContent({
|
|
className,
|
|
align = 'end',
|
|
children,
|
|
...props
|
|
}: DropdownMenuContentProps) {
|
|
const { open, setOpen } = useDropdownMenu();
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
document.addEventListener('keydown', handleEscape);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
};
|
|
}, [open, setOpen]);
|
|
|
|
if (!open) return null;
|
|
|
|
const alignClasses = {
|
|
start: 'left-0',
|
|
center: 'left-1/2 -translate-x-1/2',
|
|
end: 'right-0',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'absolute top-full mt-1 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
|
'animate-in fade-in-0 zoom-in-95',
|
|
alignClasses[align],
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DropdownMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
disabled?: boolean;
|
|
asChild?: boolean;
|
|
}
|
|
|
|
export function DropdownMenuItem({
|
|
className,
|
|
disabled,
|
|
children,
|
|
asChild,
|
|
onClick,
|
|
...props
|
|
}: DropdownMenuItemProps) {
|
|
const { setOpen } = useDropdownMenu();
|
|
|
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (disabled) return;
|
|
onClick?.(e);
|
|
setOpen(false);
|
|
};
|
|
|
|
if (asChild && React.isValidElement(children)) {
|
|
return React.cloneElement(children as React.ReactElement<any>, {
|
|
className: cn(
|
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
|
|
'hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
disabled && 'pointer-events-none opacity-50',
|
|
className,
|
|
(children as React.ReactElement<any>).props.className
|
|
),
|
|
onClick: (e: React.MouseEvent) => {
|
|
if (disabled) return;
|
|
const originalOnClick = (children as React.ReactElement<any>).props.onClick;
|
|
if (originalOnClick) originalOnClick(e);
|
|
setOpen(false);
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
|
|
'hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
disabled && 'pointer-events-none opacity-50',
|
|
className
|
|
)}
|
|
onClick={handleClick}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DropdownMenuSeparator({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function DropdownMenuLabel({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|