Skip to content
Open
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
104 changes: 100 additions & 4 deletions client/src/components/JsonView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, memo, useMemo, useCallback } from "react";
import { useState, memo, useMemo, useCallback, useEffect } from "react";
import type React from "react";
import type { JsonValue } from "@/utils/jsonUtils";
import clsx from "clsx";
import { Copy, CheckCheck } from "lucide-react";
Expand Down Expand Up @@ -51,7 +52,7 @@
variant: "destructive",
});
}
}, [toast, normalizedData]);

Check warning on line 55 in client/src/components/JsonView.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useCallback has a missing dependency: 'setCopied'. Either include it or remove the dependency array

return (
<div className={clsx("p-4 border rounded relative", className)}>
Expand Down Expand Up @@ -101,6 +102,7 @@
initialExpandDepth,
isError = false,
}: JsonNodeProps) => {
const { toast } = useToast();
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
const [typeStyleMap] = useState<Record<string, string>>({
number: "text-blue-600",
Expand All @@ -113,6 +115,52 @@
});
const dataType = getDataType(data);

const [copied, setCopied] = useState(false);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
if (copied) {
timeoutId = setTimeout(() => setCopied(false), 500);
}
return () => {
if (timeoutId) clearTimeout(timeoutId);
};
}, [copied]);

const handleCopyValue = useCallback(
(value: JsonValue) => {
try {
let text: string;
const valueType = getDataType(value);
switch (valueType) {
case "string":
text = value as unknown as string;
break;
case "number":
case "boolean":
text = String(value);
break;
case "null":
text = "null";
break;
case "undefined":
text = "undefined";
break;
default:
text = JSON.stringify(value);
}
navigator.clipboard.writeText(text);
setCopied(true);
} catch (error) {
toast({
title: "Error",
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
variant: "destructive",
});
}
},
[toast],
);

const renderCollapsible = (isArray: boolean) => {
const items = isArray
? (data as JsonValue[])
Expand Down Expand Up @@ -206,7 +254,7 @@

if (!isTooLong) {
return (
<div className="flex mr-1 rounded hover:bg-gray-800/20">
<div className="flex mr-1 rounded hover:bg-gray-800/20 group items-start">
{name && (
<span className="mr-1 text-gray-600 dark:text-gray-400">
{name}:
Expand All @@ -220,12 +268,28 @@
>
"{value}"
</pre>
<Button
variant="ghost"
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleCopyValue(value as unknown as JsonValue);
}}
aria-label={name ? `Copy value of ${name}` : "Copy value"}
title={name ? `Copy value of ${name}` : "Copy value"}
>
{copied ? (
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
) : (
<Copy className="size-4 text-foreground" />
)}
</Button>
</div>
);
}

return (
<div className="flex mr-1 rounded group hover:bg-gray-800/20">
<div className="flex mr-1 rounded group hover:bg-gray-800/20 items-start">
{name && (
<span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
{name}:
Expand All @@ -241,6 +305,22 @@
>
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
</pre>
<Button
variant="ghost"
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleCopyValue(value as unknown as JsonValue);
}}
aria-label={name ? `Copy value of ${name}` : "Copy value"}
title={name ? `Copy value of ${name}` : "Copy value"}
>
{copied ? (
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
) : (
<Copy className="size-4 text-foreground" />
)}
</Button>
</div>
);
};
Expand All @@ -253,7 +333,7 @@
return renderString(data as string);
default:
return (
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20 group">
{name && (
<span className="mr-1 text-gray-600 dark:text-gray-400">
{name}:
Expand All @@ -262,6 +342,22 @@
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
{data === null ? "null" : String(data)}
</span>
<Button
variant="ghost"
className="ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleCopyValue(data as JsonValue);
}}
aria-label={name ? `Copy value of ${name}` : "Copy value"}
title={name ? `Copy value of ${name}` : "Copy value"}
>
{copied ? (
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
) : (
<Copy className="size-4 text-foreground" />
)}
</Button>
</div>
);
}
Expand Down
Loading