Skip to content

Commit d385e99

Browse files
committed
Add UI Blocks to Context Panel
1 parent f40863c commit d385e99

File tree

5 files changed

+390
-0
lines changed

5 files changed

+390
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { type ReactNode, useCallback, useState } from "react";
2+
import { FaPython } from "react-icons/fa";
3+
4+
import { Icon } from "@/components/ui/icon";
5+
import { InlineStack } from "@/components/ui/layout";
6+
import { Paragraph } from "@/components/ui/typography";
7+
import useToastNotification from "@/hooks/useToastNotification";
8+
import type { ComponentSpec } from "@/utils/componentSpec";
9+
import { downloadYamlFromComponentText } from "@/utils/URL";
10+
import copyToYaml from "@/utils/yaml";
11+
12+
import TooltipButton from "../../Buttons/TooltipButton";
13+
14+
interface ActionBlockProps {
15+
displayName: string;
16+
componentSpec: ComponentSpec;
17+
actions?: ReactNode[];
18+
onDelete?: () => void;
19+
hasDeletionConfirmation?: boolean;
20+
readOnly?: boolean;
21+
className?: string;
22+
}
23+
24+
const ActionBlock = ({
25+
displayName,
26+
componentSpec,
27+
actions = [],
28+
onDelete,
29+
hasDeletionConfirmation = true,
30+
readOnly = false,
31+
className,
32+
}: ActionBlockProps) => {
33+
const notify = useToastNotification();
34+
const [confirmDelete, setConfirmDelete] = useState(false);
35+
36+
const pythonOriginalCode =
37+
componentSpec?.metadata?.annotations?.original_python_code;
38+
39+
const stringToPythonCodeDownload = () => {
40+
if (!pythonOriginalCode) return;
41+
42+
const blob = new Blob([pythonOriginalCode], { type: "text/x-python" });
43+
const url = URL.createObjectURL(blob);
44+
const a = document.createElement("a");
45+
a.href = url;
46+
a.download = `${componentSpec?.name || displayName}.py`;
47+
document.body.appendChild(a);
48+
a.click();
49+
document.body.removeChild(a);
50+
URL.revokeObjectURL(url);
51+
};
52+
53+
const handleDownloadYaml = () => {
54+
downloadYamlFromComponentText(componentSpec, displayName);
55+
};
56+
57+
const handleCopyYaml = () => {
58+
copyToYaml(
59+
componentSpec,
60+
(message) => notify(message, "success"),
61+
(message) => notify(message, "error"),
62+
);
63+
};
64+
65+
const handleDelete = useCallback(() => {
66+
if (confirmDelete || !hasDeletionConfirmation) {
67+
try {
68+
onDelete?.();
69+
} catch (error) {
70+
console.error("Error deleting component:", error);
71+
notify(`Error deleting component`, "error");
72+
}
73+
} else if (hasDeletionConfirmation) {
74+
setConfirmDelete(true);
75+
}
76+
}, [onDelete, confirmDelete, hasDeletionConfirmation, notify]);
77+
78+
return (
79+
<InlineStack gap="2" className={className}>
80+
<TooltipButton
81+
variant="outline"
82+
tooltip="Download YAML"
83+
onClick={handleDownloadYaml}
84+
>
85+
<Icon name="Download" />
86+
</TooltipButton>
87+
88+
{pythonOriginalCode && (
89+
<TooltipButton
90+
variant="outline"
91+
tooltip="Download Python Code"
92+
onClick={stringToPythonCodeDownload}
93+
>
94+
<FaPython />
95+
</TooltipButton>
96+
)}
97+
98+
<TooltipButton
99+
variant="outline"
100+
tooltip="Copy YAML"
101+
onClick={handleCopyYaml}
102+
>
103+
<Icon name="Clipboard" />
104+
</TooltipButton>
105+
106+
{actions}
107+
108+
{onDelete && !readOnly && (
109+
<TooltipButton
110+
variant="destructive"
111+
tooltip={
112+
confirmDelete || !hasDeletionConfirmation
113+
? "Confirm Delete. This action cannot be undone."
114+
: "Delete Component"
115+
}
116+
onClick={handleDelete}
117+
>
118+
<InlineStack gap="2" blockAlign="center">
119+
<Icon name="Trash" />
120+
{confirmDelete && hasDeletionConfirmation && (
121+
<Paragraph size="xs">Confirm Delete</Paragraph>
122+
)}
123+
</InlineStack>
124+
</TooltipButton>
125+
)}
126+
</InlineStack>
127+
);
128+
};
129+
130+
export default ActionBlock;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { type ReactNode } from "react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Collapsible,
6+
CollapsibleContent,
7+
CollapsibleTrigger,
8+
} from "@/components/ui/collapsible";
9+
import { Icon } from "@/components/ui/icon";
10+
import { BlockStack, InlineStack } from "@/components/ui/layout";
11+
import { Heading } from "@/components/ui/typography";
12+
13+
type ContentBlockProps =
14+
| {
15+
title: string;
16+
children?: ReactNode;
17+
collapsible?: false;
18+
defaultOpen?: never;
19+
className?: string;
20+
}
21+
| {
22+
title: string;
23+
children?: ReactNode;
24+
collapsible: true;
25+
defaultOpen?: boolean;
26+
className?: string;
27+
};
28+
29+
export const ContentBlock = ({
30+
title,
31+
children,
32+
collapsible,
33+
defaultOpen = false,
34+
className,
35+
}: ContentBlockProps) => {
36+
if (!children) {
37+
return null;
38+
}
39+
40+
return (
41+
<BlockStack className={className}>
42+
<Collapsible className="w-full" defaultOpen={defaultOpen}>
43+
<InlineStack blockAlign="center" gap="1">
44+
<Heading level={3}>{title}</Heading>
45+
{collapsible && (
46+
<CollapsibleTrigger asChild>
47+
<Button variant="ghost" size="sm">
48+
<Icon name="ChevronsUpDown" />
49+
<span className="sr-only">Toggle</span>
50+
</Button>
51+
</CollapsibleTrigger>
52+
)}
53+
</InlineStack>
54+
55+
{collapsible ? (
56+
<CollapsibleContent className="w-full mt-1">
57+
{children}
58+
</CollapsibleContent>
59+
) : (
60+
children
61+
)}
62+
</Collapsible>
63+
</BlockStack>
64+
);
65+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { InlineStack } from "@/components/ui/layout";
2+
import { Link } from "@/components/ui/link";
3+
import { Paragraph } from "@/components/ui/typography";
4+
import { cn } from "@/lib/utils";
5+
6+
import { CopyText } from "../../CopyText/CopyText";
7+
8+
export interface KeyValuePairProps {
9+
label?: string;
10+
value?: string | { href: string; text: string };
11+
critical?: boolean;
12+
copyable?: boolean;
13+
}
14+
15+
export const KeyValuePair = ({
16+
label,
17+
value,
18+
critical,
19+
copyable,
20+
}: KeyValuePairProps) => {
21+
if (!value) {
22+
return null;
23+
}
24+
25+
return (
26+
<InlineStack gap="2" blockAlign="center" wrap="nowrap">
27+
{label && (
28+
<Paragraph
29+
size="xs"
30+
tone={critical ? "critical" : "inherit"}
31+
className="shrink-0 whitespace-nowrap"
32+
>
33+
{label}:
34+
</Paragraph>
35+
)}
36+
37+
<div className="min-w-0 flex-1 overflow-hidden">
38+
{isLink(value) ? (
39+
<Link
40+
href={value.href}
41+
size="xs"
42+
variant="classic"
43+
external
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
>
47+
{value.text}
48+
</Link>
49+
) : copyable ? (
50+
<CopyText
51+
className={cn(
52+
"text-xs truncate",
53+
critical ? "text-destructive" : "text-muted-foreground",
54+
)}
55+
>
56+
{value}
57+
</CopyText>
58+
) : (
59+
<Paragraph
60+
size="xs"
61+
tone={critical ? "critical" : "subdued"}
62+
className="truncate"
63+
>
64+
{value}
65+
</Paragraph>
66+
)}
67+
</div>
68+
</InlineStack>
69+
);
70+
};
71+
72+
const isLink = (
73+
val: string | { href: string; text: string },
74+
): val is { href: string; text: string } => {
75+
return typeof val === "object" && val !== null && "href" in val;
76+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BlockStack } from "@/components/ui/layout";
2+
import { Heading } from "@/components/ui/typography";
3+
4+
import { KeyValuePair, type KeyValuePairProps } from "./KeyValuePair";
5+
6+
interface ListBlockProps {
7+
title?: string;
8+
items: KeyValuePairProps[];
9+
marker?: "bullet" | "number" | "none";
10+
className?: string;
11+
}
12+
13+
export const ListBlock = ({
14+
title,
15+
items,
16+
marker = "bullet",
17+
className,
18+
}: ListBlockProps) => {
19+
const listElement = marker === "number" ? "ol" : "ul";
20+
21+
const getListStyle = () => {
22+
switch (marker) {
23+
case "bullet":
24+
return "pl-5 list-disc";
25+
case "number":
26+
return "pl-5 list-decimal";
27+
case "none":
28+
return "list-none";
29+
default:
30+
return "pl-5 list-disc";
31+
}
32+
};
33+
34+
return (
35+
<BlockStack className={className}>
36+
{title && <Heading level={3}>{title}</Heading>}
37+
<BlockStack as={listElement} gap="1" className={getListStyle()}>
38+
{items.map((item, index) => {
39+
if (!item.value) {
40+
return null;
41+
}
42+
43+
return (
44+
<li key={index}>
45+
<KeyValuePair {...item} />
46+
</li>
47+
);
48+
})}
49+
</BlockStack>
50+
</BlockStack>
51+
);
52+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Collapsible,
4+
CollapsibleContent,
5+
CollapsibleTrigger,
6+
} from "@/components/ui/collapsible";
7+
import { Icon } from "@/components/ui/icon";
8+
import { BlockStack, InlineStack } from "@/components/ui/layout";
9+
import { Heading, Paragraph } from "@/components/ui/typography";
10+
11+
import { CopyText } from "../../CopyText/CopyText";
12+
13+
interface TextBlockProps {
14+
title: string;
15+
text?: string;
16+
copyable?: boolean;
17+
collapsible?: boolean;
18+
className?: string;
19+
}
20+
21+
export const TextBlock = ({
22+
title,
23+
text,
24+
copyable,
25+
collapsible,
26+
className,
27+
}: TextBlockProps) => {
28+
if (!text) {
29+
return null;
30+
}
31+
32+
const content = copyable ? (
33+
<CopyText className="text-xs text-muted-foreground truncate">
34+
{text}
35+
</CopyText>
36+
) : (
37+
<Paragraph tone="subdued" size="xs" className="truncate">
38+
{text}
39+
</Paragraph>
40+
);
41+
42+
return (
43+
<BlockStack className={className}>
44+
<Collapsible className="w-full">
45+
<InlineStack blockAlign="center" gap="1">
46+
<Heading level={3}>{title}</Heading>
47+
{collapsible && (
48+
<CollapsibleTrigger asChild>
49+
<Button variant="ghost" size="sm">
50+
<Icon name="ChevronsUpDown" />
51+
<span className="sr-only">Toggle</span>
52+
</Button>
53+
</CollapsibleTrigger>
54+
)}
55+
</InlineStack>
56+
57+
{collapsible ? (
58+
<CollapsibleContent className="w-full mt-1">
59+
{content}
60+
</CollapsibleContent>
61+
) : (
62+
content
63+
)}
64+
</Collapsible>
65+
</BlockStack>
66+
);
67+
};

0 commit comments

Comments
 (0)