feat(databases): add copy button to User and Database Name fields (#4735)

* 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>
This commit is contained in:
Mauricio Siu
2026-07-05 16:34:35 -06:00
committed by GitHub
co-authored by autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
parent aa93897eae
commit cb23d726fe
8 changed files with 79 additions and 54 deletions
@@ -28,7 +28,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Sqld Node</Label> <Label>Sqld Node</Label>
@@ -71,7 +71,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Enable Namespaces</Label> <Label>Enable Namespaces</Label>
@@ -25,11 +25,11 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">
@@ -25,7 +25,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -55,7 +55,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">
@@ -25,11 +25,11 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">
@@ -25,11 +25,11 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -57,7 +57,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -25,7 +25,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value="default" /> <Input enableCopyButton disabled value="default" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -53,7 +53,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">
+61 -38
View File
@@ -1,13 +1,17 @@
import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react"; import copy from "copy-to-clipboard";
import { Clipboard, EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner";
import { generateRandomPassword } from "@/lib/password-utils"; import { generateRandomPassword } from "@/lib/password-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "./button";
export interface InputProps extends React.ComponentProps<"input"> { export interface InputProps extends React.ComponentProps<"input"> {
errorMessage?: string; errorMessage?: string;
enablePasswordGenerator?: boolean; enablePasswordGenerator?: boolean;
passwordGeneratorLength?: number; passwordGeneratorLength?: number;
enableCopyButton?: boolean;
} }
function Input({ function Input({
@@ -16,6 +20,7 @@ function Input({
errorMessage, errorMessage,
enablePasswordGenerator = false, enablePasswordGenerator = false,
passwordGeneratorLength, passwordGeneratorLength,
enableCopyButton = false,
ref, ref,
...props ...props
}: InputProps) { }: InputProps) {
@@ -65,49 +70,67 @@ function Input({
input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true }));
}; };
return ( const handleCopy = () => {
<> copy(inputRef.current?.value || "");
<div className="relative w-full"> toast.success("Value is copied to clipboard");
<input };
type={inputType}
data-slot="input" const inputElement = (
className={cn( <div className="relative w-full">
"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", <input
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"), type={inputType}
className, data-slot="input"
)} className={cn(
ref={setRefs} "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",
{...props} isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
/> className,
{isPassword && ( )}
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground"> ref={setRefs}
{shouldShowGenerator && ( {...props}
<button />
type="button" {isPassword && (
className="hover:text-foreground focus:outline-none" <div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
onClick={handleGeneratePassword} {shouldShowGenerator && (
aria-label="Generate password"
title="Generate password"
tabIndex={-1}
>
<RefreshCcw className="h-4 w-4" />
</button>
)}
<button <button
type="button" type="button"
className="hover:text-foreground focus:outline-none" className="hover:text-foreground focus:outline-none"
onClick={() => setShowPassword(!showPassword)} onClick={handleGeneratePassword}
aria-label="Generate password"
title="Generate password"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? ( <RefreshCcw className="h-4 w-4" />
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</button> </button>
</div> )}
)} <button
</div> 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 && ( {errorMessage && (
<span className="text-sm text-red-600 text-secondary-foreground"> <span className="text-sm text-red-600 text-secondary-foreground">
{errorMessage} {errorMessage}
@@ -136,7 +136,9 @@ export const deploymentRouter = createTRPCRouter({
}); });
} else if (schedule.serverId) { } else if (schedule.serverId) {
const targetServer = await findServerById(schedule.serverId); const targetServer = await findServerById(schedule.serverId);
if (targetServer.organizationId !== ctx.session.activeOrganizationId) { if (
targetServer.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this schedule.", message: "You don't have access to this schedule.",