Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type FormValues = z.infer<typeof overrideValidationSchema>;
type Props = PropsWithChildren<{
isModalOpen: boolean;
onOpenChange: (value: boolean) => void;
identifier: string;
identifier?: string;
isLoading?: boolean;
namespaceId: string;
overrideDetails?: OverrideDetails | null;
Expand Down Expand Up @@ -163,8 +163,8 @@ export const IdentifierDialog = ({
description="The identifier you use when ratelimiting."
error={errors.identifier?.message}
{...register("identifier")}
readOnly
disabled
readOnly={Boolean(identifier)}
disabled={Boolean(identifier)}
/>

<FormInput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
"use client";

import { CopyButton } from "@/components/dashboard/copy-button";
import { QuickNavPopover } from "@/components/navbar-popover";
import { CopyableIDButton } from "@/components/navigation/copyable-id-button";
import { Navbar } from "@/components/navigation/navbar";
import { Badge } from "@/components/ui/badge";
import { ChevronExpandY, Gauge } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { useState } from "react";
import { IdentifierDialog } from "./_components/identifier-dialog";

export const NamespaceNavbar = ({
namespace,
ratelimitNamespaces,
activePage,
}: {
type NamespaceNavbarProps = {
namespace: {
id: string;
name: string;
Expand All @@ -24,7 +21,14 @@ export const NamespaceNavbar = ({
href: string;
text: string;
};
}) => {
};

export const NamespaceNavbar = ({
namespace,
ratelimitNamespaces,
activePage,
}: NamespaceNavbarProps) => {
const [open, setOpen] = useState<boolean>(false);
return (
<>
<Navbar>
Expand Down Expand Up @@ -76,16 +80,18 @@ export const NamespaceNavbar = ({
</Navbar.Breadcrumbs.Link>
</Navbar.Breadcrumbs>
<Navbar.Actions>
<Badge
key="namespaceId"
variant="secondary"
className="flex justify-between w-full gap-2 font-mono font-medium ph-no-capture"
<Button
onClick={() => setOpen(true)}
variant="outline"
size="md"
className="bg-grayA-2 hover:bg-grayA-3"
>
{namespace.id}
<CopyButton value={namespace.id} />
</Badge>
Override Identifier
</Button>
<CopyableIDButton value={namespace.id} />
</Navbar.Actions>
</Navbar>
<IdentifierDialog onOpenChange={setOpen} isModalOpen={open} namespaceId={namespace.id} />
</>
);
};
72 changes: 40 additions & 32 deletions apps/dashboard/components/dashboard/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { Copy, CopyCheck } from "lucide-react";
import { TaskChecked, TaskUnchecked } from "@unkey/icons";
import * as React from "react";

interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
value: string;
Expand All @@ -14,33 +12,43 @@ async function copyToClipboardWithMeta(value: string, _meta?: Record<string, unk
navigator.clipboard.writeText(value);
}

export function CopyButton({ value, className, src, ...props }: CopyButtonProps) {
const [copied, setCopied] = React.useState(false);
export const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
({ value, className, src, ...props }, ref) => {
const [copied, setCopied] = React.useState(false);

React.useEffect(() => {
if (!copied) {
return;
}
const timer = setTimeout(() => {
setCopied(false);
}, 2000);
return () => clearTimeout(timer);
}, [copied]);
React.useEffect(() => {
if (!copied) {
return;
}
const timer = setTimeout(() => {
setCopied(false);
}, 2000);
return () => clearTimeout(timer);
}, [copied]);

return (
<button
type="button"
className={cn("relative p-1 focus:outline-none h-6 w-6 ", className)}
onClick={() => {
copyToClipboardWithMeta(value, {
component: src,
});
setCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{copied ? <CopyCheck className="w-full h-full" /> : <Copy className="w-full h-full" />}
</button>
);
}
return (
<button
type="button"
ref={ref}
className={cn("relative p-1 focus:outline-none h-6 w-6 ", className)}
onClick={(e) => {
e.stopPropagation(); // Prevent triggering parent button click
copyToClipboardWithMeta(value, {
component: src,
});
setCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{copied ? (
<TaskChecked className="w-full h-full" />
) : (
<TaskUnchecked className="w-full h-full" />
)}
</button>
);
},
);

CopyButton.displayName = "CopyButton";
80 changes: 80 additions & 0 deletions apps/dashboard/components/navigation/copyable-id-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Button } from "@unkey/ui";
import { useRef } from "react";
import { CopyButton } from "../dashboard/copy-button";

type CopyableIDButtonProps = {
value: string;
className?: string;
};

export const CopyableIDButton = ({ value, className = "" }: CopyableIDButtonProps) => {
const textRef = useRef<HTMLDivElement>(null);
const pressTimer = useRef<NodeJS.Timeout | null>(null);
const copyButtonRef = useRef<HTMLButtonElement>(null);

const handleMouseDown = () => {
// Start a long-press timer
pressTimer.current = setTimeout(() => {
// For long-press, select the text
if (textRef.current) {
const range = document.createRange();
range.selectNodeContents(textRef.current);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}
}, 500);
};

const handleMouseUp = () => {
// Clear the timer if mouse is released before long-press threshold
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
};

const handleMouseLeave = () => {
// Clear the timer if mouse leaves the button
if (pressTimer.current) {
clearTimeout(pressTimer.current);
pressTimer.current = null;
}
};

const handleClick = (e: React.MouseEvent) => {
// Only handle click if it wasn't a long press
if (!window.getSelection()?.toString()) {
// Programmatically click the CopyButton if text isn't selected
copyButtonRef.current?.click();
} else {
// If text is selected, don't trigger the copy
e.stopPropagation();
}
};

return (
<Button
variant="outline"
size="md"
className={`text-xs font-mono font-medium ph-no-capture h-8 bg-grayA-2 hover:bg-grayA-3 ${className}`}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
>
<div className="flex gap-2 items-center justify-center">
<div ref={textRef} className="select-text">
{value}
</div>
<CopyButton
value={value}
ref={copyButtonRef}
className="pointer-events-none" // Make the button non-interactive directly
/>
</div>
</Button>
);
};