mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-17 20:05:24 +02:00
* feat(databases): add copy button to User and Database Name fields Adds an enableCopyButton prop to the Input component and uses it on the User, Database Name, and Internal Host fields across Postgres, MySQL, MariaDB, MongoDB, Redis, and Libsql credential views. Closes #4495 * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
import copy from "copy-to-clipboard";
|
|
import { Clipboard, EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
|
|
import * as React from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { generateRandomPassword } from "@/lib/password-utils";
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "./button";
|
|
|
|
export interface InputProps extends React.ComponentProps<"input"> {
|
|
errorMessage?: string;
|
|
enablePasswordGenerator?: boolean;
|
|
passwordGeneratorLength?: number;
|
|
enableCopyButton?: boolean;
|
|
}
|
|
|
|
function Input({
|
|
className,
|
|
type,
|
|
errorMessage,
|
|
enablePasswordGenerator = false,
|
|
passwordGeneratorLength,
|
|
enableCopyButton = false,
|
|
ref,
|
|
...props
|
|
}: InputProps) {
|
|
const [showPassword, setShowPassword] = React.useState(false);
|
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
const isPassword = type === "password";
|
|
const shouldShowGenerator =
|
|
isPassword &&
|
|
enablePasswordGenerator !== false &&
|
|
!props.disabled &&
|
|
!props.readOnly;
|
|
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
|
|
|
const setRefs = React.useCallback(
|
|
(node: HTMLInputElement | null) => {
|
|
inputRef.current = node;
|
|
if (typeof ref === "function") {
|
|
ref(node);
|
|
} else if (ref && typeof ref === "object") {
|
|
(ref as { current: HTMLInputElement | null }).current = node;
|
|
}
|
|
},
|
|
[ref],
|
|
);
|
|
|
|
const handleGeneratePassword = () => {
|
|
const nextValue =
|
|
typeof passwordGeneratorLength === "number" && passwordGeneratorLength > 0
|
|
? generateRandomPassword(Math.floor(passwordGeneratorLength))
|
|
: generateRandomPassword();
|
|
|
|
const input = inputRef.current;
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
const valueSetter = Object.getOwnPropertyDescriptor(
|
|
HTMLInputElement.prototype,
|
|
"value",
|
|
)?.set;
|
|
if (valueSetter) {
|
|
valueSetter.call(input, nextValue);
|
|
} else {
|
|
input.value = nextValue;
|
|
}
|
|
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
};
|
|
|
|
const handleCopy = () => {
|
|
copy(inputRef.current?.value || "");
|
|
toast.success("Value is copied to clipboard");
|
|
};
|
|
|
|
const inputElement = (
|
|
<div className="relative w-full">
|
|
<input
|
|
type={inputType}
|
|
data-slot="input"
|
|
className={cn(
|
|
"h-10 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-2 text-sm transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
|
|
className,
|
|
)}
|
|
ref={setRefs}
|
|
{...props}
|
|
/>
|
|
{isPassword && (
|
|
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
|
|
{shouldShowGenerator && (
|
|
<button
|
|
type="button"
|
|
className="hover:text-foreground focus:outline-none"
|
|
onClick={handleGeneratePassword}
|
|
aria-label="Generate password"
|
|
title="Generate password"
|
|
tabIndex={-1}
|
|
>
|
|
<RefreshCcw className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="hover:text-foreground focus:outline-none"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOffIcon className="h-4 w-4" />
|
|
) : (
|
|
<EyeIcon className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{enableCopyButton ? (
|
|
<div className="flex w-full items-center space-x-2">
|
|
{inputElement}
|
|
<Button type="button" variant={"secondary"} onClick={handleCopy}>
|
|
<Clipboard className="size-4 text-muted-foreground" />
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
inputElement
|
|
)}
|
|
{errorMessage && (
|
|
<span className="text-sm text-red-600 text-secondary-foreground">
|
|
{errorMessage}
|
|
</span>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function NumberInput({ className, ref, ...props }: InputProps) {
|
|
return (
|
|
<Input
|
|
type="text"
|
|
className={cn("text-left", className)}
|
|
ref={ref}
|
|
{...props}
|
|
value={props.value === undefined ? undefined : String(props.value)}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
if (value === "") {
|
|
props.onChange?.(e);
|
|
} else {
|
|
const number = Number.parseInt(value, 10);
|
|
if (!Number.isNaN(number)) {
|
|
const syntheticEvent = {
|
|
...e,
|
|
target: {
|
|
...e.target,
|
|
value: number,
|
|
},
|
|
};
|
|
props.onChange?.(
|
|
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
|
);
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export { Input, NumberInput };
|