Skip to content

Commit 5835d79

Browse files
feat: copy button in dag nodes (#696)
Co-authored-by: Cahllagerfeld <[email protected]>
1 parent f0a0a38 commit 5835d79

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed

src/components/dag-visualizer/ArtifactNode.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import Copy from "@/assets/icons/copy.svg?react";
12
import { ArtifactVersion } from "@/types/artifact-versions";
2-
33
import { NodeProps, useStore } from "reactflow";
44
import { ArtifactIcon } from "../ArtifactIcon";
55
import { ArtifactSheet } from "../artifacts/artifact-node-sheet";
66
import { BaseNode } from "./BaseNode";
7+
import { CopyNodeButton } from "./NodeCopyButton";
78

89
export function ArtifactNode({ data, selected }: NodeProps<ArtifactVersion & { name: string }>) {
910
const { unselectNodesAndEdges } = useStore((state) => ({
@@ -42,11 +43,23 @@ export function ArtifactNode({ data, selected }: NodeProps<ArtifactVersion & { n
4243
<p className="truncate text-text-sm font-semibold text-theme-text-brand group-data-[selected=true]:text-theme-text-negative">
4344
{data.name}
4445
</p>
46+
4547
<p className="truncate text-text-xs text-theme-text-secondary group-data-[selected=true]:text-white/70">
4648
{/* As artifact_type doesn't correspond to the last part of the string */}
4749
{getTypeFromArtifact(data.body?.data_type.attribute || "")}
4850
</p>
4951
</div>
52+
<CopyNodeButton
53+
className="h-4 w-4 shrink-0 rounded-sm hover:bg-primary-100 active:bg-primary-200"
54+
code={`from zenml.client import Client
55+
56+
artifact = Client().get_artifact_version('${data.id}')
57+
loaded_artifact = artifact.load()`}
58+
type="artifact"
59+
>
60+
<Copy className="h-3 w-3 fill-primary-400" />
61+
<div className="sr-only">Copy code to load artifact</div>
62+
</CopyNodeButton>
5063
</button>
5164
</ArtifactSheet>
5265
</BaseNode>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { HTMLAttributes, useState } from "react";
2+
import {
3+
Tooltip,
4+
TooltipContent,
5+
TooltipProvider,
6+
TooltipTrigger
7+
} from "@zenml-io/react-component-library/components/client";
8+
import { cn } from "@zenml-io/react-component-library/utilities";
9+
10+
export function CopyNodeButton({
11+
className,
12+
code,
13+
type,
14+
...rest
15+
}: HTMLAttributes<HTMLDivElement> & { code: string; type: "artifact" | "step" }) {
16+
const [copied, setCopied] = useState(false);
17+
const [tooltipOpen, setTooltipOpen] = useState(false);
18+
19+
function copyToClipboard(code: string) {
20+
if (navigator.clipboard) {
21+
navigator.clipboard.writeText(code);
22+
setCopied(true);
23+
setTooltipOpen(true);
24+
25+
setTimeout(() => {
26+
setTooltipOpen(false);
27+
setCopied(false);
28+
}, 2000);
29+
}
30+
}
31+
return (
32+
<TooltipProvider>
33+
<Tooltip open={tooltipOpen} onOpenChange={setTooltipOpen}>
34+
<TooltipTrigger asChild>
35+
<div
36+
{...rest}
37+
className={cn(
38+
"hidden cursor-pointer items-center justify-center transition-all group-hover:flex",
39+
className
40+
)}
41+
onClick={(e) => {
42+
e.stopPropagation(); // Prevent the click event from bubbling up
43+
copyToClipboard(code);
44+
}}
45+
></div>
46+
</TooltipTrigger>
47+
<TooltipContent className="w-full max-w-md whitespace-normal transition-opacity">
48+
{copied ? "Copied!" : `Copy code to load ${type}`}
49+
</TooltipContent>
50+
</Tooltip>
51+
</TooltipProvider>
52+
);
53+
}

src/components/dag-visualizer/StepNode.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import Copy from "@/assets/icons/copy.svg?react";
12
import { calculateTimeDifference } from "@/lib/dates";
23
import { Step } from "@/types/steps";
34
import { clsx } from "clsx";
45
import { NodeProps, ReactFlowState, useStore } from "reactflow";
56
import { ExecutionStatusIcon, getExecutionStatusBackgroundColor } from "../ExecutionStatus";
67
import { StepSheet } from "../steps/step-sheet";
78
import { BaseNode } from "./BaseNode";
9+
import { CopyNodeButton } from "./NodeCopyButton";
810

911
const selector = (state: ReactFlowState) => ({
1012
unselectAll: state.unselectNodesAndEdges
@@ -30,7 +32,7 @@ export function StepNode({ data, selected }: NodeProps<Step>) {
3032
data-failed={isFailed}
3133
data-selected={!!selected}
3234
className={clsx(
33-
"max-h-[80px] max-w-[300px] overflow-hidden rounded-md border bg-theme-surface-primary transition-all duration-200 hover:shadow-md data-[selected=true]:shadow-md",
35+
"group max-h-[80px] max-w-[300px] overflow-hidden rounded-md border bg-theme-surface-primary transition-all duration-200 hover:shadow-md data-[selected=true]:shadow-md",
3436
{
3537
"border-theme-border-moderate hover:border-neutral-400 data-[selected=true]:border-theme-border-bold":
3638
!isFailed,
@@ -45,6 +47,16 @@ export function StepNode({ data, selected }: NodeProps<Step>) {
4547
<ExecutionStatusIcon status={data.body?.status} className="h-4 w-4" />
4648
</div>
4749
<p className="truncate font-semibold">{data.name}</p>
50+
<CopyNodeButton
51+
className="h-4 w-4 shrink-0 rounded-sm hover:bg-theme-surface-secondary active:bg-neutral-300"
52+
code={`from zenml.client import Client
53+
step = Client().get_run_step(${data.id})
54+
config = step.config`}
55+
type="step"
56+
>
57+
<Copy className="h-3 w-3 fill-theme-text-tertiary" />
58+
<div className="sr-only">Copy code to load step</div>
59+
</CopyNodeButton>
4860
</div>
4961
<div
5062
data-failed={isFailed}

0 commit comments

Comments
 (0)