1- import { EyeIcon , EyeOffIcon , PlusIcon , Trash2Icon } from "lucide-react" ;
1+ import { CheckIcon , CopyIcon , EyeIcon , EyeOffIcon , PlusIcon , Trash2Icon } from "lucide-react" ;
22import { type CSSProperties , type ReactNode , useEffect , useState } from "react" ;
33import {
44 ENVIRONMENT_VARIABLE_KEY_MAX_LENGTH ,
@@ -11,6 +11,7 @@ import { Button } from "./ui/button";
1111import { Input } from "./ui/input" ;
1212import { Textarea } from "./ui/textarea" ;
1313import { Tooltip , TooltipPopup , TooltipTrigger } from "./ui/tooltip" ;
14+ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard" ;
1415import { cn } from "~/lib/utils" ;
1516
1617type DraftRow = {
@@ -125,6 +126,14 @@ export function EnvironmentVariablesEditor({
125126 const [ visibleValueRowIds , setVisibleValueRowIds ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
126127 const [ isSaving , setIsSaving ] = useState ( false ) ;
127128 const [ saveError , setSaveError ] = useState < string | null > ( null ) ;
129+ const [ copiedRowId , setCopiedRowId ] = useState < string | null > ( null ) ;
130+ const { copyToClipboard } = useCopyToClipboard < string > ( {
131+ timeout : 2000 ,
132+ onCopy : ( rowId ) => {
133+ setCopiedRowId ( rowId ) ;
134+ setTimeout ( ( ) => setCopiedRowId ( null ) , 2000 ) ;
135+ } ,
136+ } ) ;
128137
129138 useEffect ( ( ) => {
130139 setRows ( rowsFromEntries ( entries ) ) ;
@@ -278,31 +287,57 @@ export function EnvironmentVariablesEditor({
278287 >
279288 Value
280289 </ label >
281- < Tooltip >
282- < TooltipTrigger
283- render = {
284- < Button
285- type = "button"
286- size = "icon-xs"
287- variant = "ghost"
288- className = "size-6 rounded-md text-muted-foreground hover:text-foreground"
289- aria-label = { isValueVisible ? "Hide value" : "Show value" }
290- aria-pressed = { isValueVisible }
291- disabled = { isReadonly }
292- onClick = { ( ) => toggleValueVisibility ( row . id ) }
293- >
294- { isValueVisible ? (
295- < EyeOffIcon className = "size-3.5" />
296- ) : (
297- < EyeIcon className = "size-3.5" />
298- ) }
299- </ Button >
300- }
301- />
302- < TooltipPopup side = "top" >
303- { isValueVisible ? "Hide value" : "Show value" }
304- </ TooltipPopup >
305- </ Tooltip >
290+ < div className = "flex items-center gap-0.5" >
291+ < Tooltip >
292+ < TooltipTrigger
293+ render = {
294+ < Button
295+ type = "button"
296+ size = "icon-xs"
297+ variant = "ghost"
298+ className = "size-6 rounded-md text-muted-foreground hover:text-foreground"
299+ aria-label = { isValueVisible ? "Hide value" : "Show value" }
300+ aria-pressed = { isValueVisible }
301+ disabled = { isReadonly }
302+ onClick = { ( ) => toggleValueVisibility ( row . id ) }
303+ >
304+ { isValueVisible ? (
305+ < EyeOffIcon className = "size-3.5" />
306+ ) : (
307+ < EyeIcon className = "size-3.5" />
308+ ) }
309+ </ Button >
310+ }
311+ />
312+ < TooltipPopup side = "top" >
313+ { isValueVisible ? "Hide value" : "Show value" }
314+ </ TooltipPopup >
315+ </ Tooltip >
316+ < Tooltip >
317+ < TooltipTrigger
318+ render = {
319+ < Button
320+ type = "button"
321+ size = "icon-xs"
322+ variant = "ghost"
323+ className = "size-6 rounded-md text-muted-foreground hover:text-foreground"
324+ aria-label = "Copy value"
325+ disabled = { isReadonly || row . value . length === 0 }
326+ onClick = { ( ) => copyToClipboard ( row . value , row . id ) }
327+ >
328+ { copiedRowId === row . id ? (
329+ < CheckIcon className = "size-3.5 text-green-500" />
330+ ) : (
331+ < CopyIcon className = "size-3.5" />
332+ ) }
333+ </ Button >
334+ }
335+ />
336+ < TooltipPopup side = "top" >
337+ { copiedRowId === row . id ? "Copied!" : "Copy value" }
338+ </ TooltipPopup >
339+ </ Tooltip >
340+ </ div >
306341 </ div >
307342 < Textarea
308343 id = { valueFieldId }
0 commit comments