Skip to content

Commit 87a5600

Browse files
committed
Add UI Blocks to Context Panel
1 parent ecc2b18 commit 87a5600

File tree

4 files changed

+405
-0
lines changed

4 files changed

+405
-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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { BlockStack, InlineStack } from "@/components/ui/layout";
2+
import { Link } from "@/components/ui/link";
3+
import { Heading, Paragraph } from "@/components/ui/typography";
4+
5+
export interface ListBlockItemProps {
6+
name?: string;
7+
value?: string | { href: string; text: string };
8+
critical?: boolean;
9+
}
10+
11+
interface ListBlockProps {
12+
title?: string;
13+
items: ListBlockItemProps[];
14+
marker?: "bullet" | "number" | "none";
15+
className?: string;
16+
}
17+
18+
export const ListBlock = ({
19+
title,
20+
items,
21+
marker = "bullet",
22+
className,
23+
}: ListBlockProps) => {
24+
const listElement = marker === "number" ? "ol" : "ul";
25+
26+
const getListStyle = () => {
27+
switch (marker) {
28+
case "bullet":
29+
return "pl-5 list-disc";
30+
case "number":
31+
return "pl-5 list-decimal";
32+
case "none":
33+
return "list-none";
34+
default:
35+
return "pl-5 list-disc";
36+
}
37+
};
38+
39+
return (
40+
<BlockStack className={className}>
41+
{title && <Heading level={3}>{title}</Heading>}
42+
<BlockStack as={listElement} gap="1" className={getListStyle()}>
43+
{items.map((item, index) => {
44+
if (!item.value) {
45+
return null;
46+
}
47+
48+
return (
49+
<li key={index} className="w-full">
50+
<ListBlockItem
51+
name={item.name}
52+
value={item.value}
53+
critical={item.critical}
54+
/>
55+
</li>
56+
);
57+
})}
58+
</BlockStack>
59+
</BlockStack>
60+
);
61+
};
62+
63+
const ListBlockItem = ({ name, value, critical }: ListBlockItemProps) => {
64+
if (!value) {
65+
return null;
66+
}
67+
68+
return (
69+
<InlineStack gap="2" blockAlign="center" wrap="nowrap">
70+
{name && (
71+
<Paragraph size="xs" tone={critical ? "critical" : "inherit"}>
72+
{name}:
73+
</Paragraph>
74+
)}
75+
{isLink(value) ? (
76+
<Link
77+
href={value.href}
78+
size="xs"
79+
variant="classic"
80+
external
81+
target="_blank"
82+
rel="noopener noreferrer"
83+
>
84+
{value.text}
85+
</Link>
86+
) : (
87+
<Paragraph
88+
size="xs"
89+
tone={critical ? "critical" : "subdued"}
90+
className="truncate"
91+
>
92+
{value}
93+
</Paragraph>
94+
)}
95+
</InlineStack>
96+
);
97+
};
98+
99+
const isLink = (
100+
val: string | { href: string; text: string },
101+
): val is { href: string; text: string } => {
102+
return typeof val === "object" && val !== null && "href" in val;
103+
};

0 commit comments

Comments
 (0)