Skip to content

Commit 95f54cb

Browse files
authored
component editor - meta editor for python components (#1173)
## Description Closes #1111 Closes #1110 Closes #1109 Enhanced the Python Component Editor with a new metadata tab that allows users to configure container settings. This update enables users to: 1. Specify a custom base image for the container 2. Add Python packages to be installed in the container 3. Define custom annotations for the component The PR also adds functionality to parse and preserve existing metadata when editing Python components, ensuring that previously configured settings are maintained. ## Type of Change - [x] New feature - [x] Improvement ## Checklist - [x] I have tested this does not break current pipelines / runs functionality - [x] I have tested the changes on staging ## Test Instructions [Screen Recording 2025-10-21 at 7.19.00 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/dd65e1e9-8961-4ba0-b2c1-3ea0ade47e45.mov" />](https://app.graphite.dev/user-attachments/video/dd65e1e9-8961-4ba0-b2c1-3ea0ade47e45.mov) 1. Open the Component Editor for a Python component 2. Switch to the "Metadata" tab 3. Verify you can set a custom container image 4. Add Python packages to be installed 5. Add custom annotations 6. Save the component and verify the settings are preserved
1 parent e06eeee commit 95f54cb

File tree

5 files changed

+413
-25
lines changed

5 files changed

+413
-25
lines changed

src/components/shared/ComponentEditor/ComponentEditorDialog.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import { Heading, Paragraph } from "@/components/ui/typography";
1010
import useToastNotification from "@/hooks/useToastNotification";
1111
import { useComponentLibrary } from "@/providers/ComponentLibraryProvider";
1212
import { hydrateComponentReference } from "@/services/componentService";
13+
import { isContainerImplementation } from "@/utils/componentSpec";
1314
import { saveComponent } from "@/utils/localforage";
1415

1516
import { FullscreenElement } from "../FullscreenElement";
1617
import { withSuspenseWrapper } from "../SuspenseWrapper";
1718
import { PythonComponentEditor } from "./components/PythonComponentEditor";
1819
import { YamlComponentEditor } from "./components/YamlComponentEditor";
19-
import type { SupportedTemplate } from "./types";
20+
import type { SupportedTemplate, YamlGeneratorOptions } from "./types";
2021
import { useTemplateCodeByName } from "./useTemplateCodeByName";
2122

2223
const ComponentEditorDialogSkeleton = () => {
@@ -58,7 +59,11 @@ const ComponentEditorDialogSkeleton = () => {
5859
};
5960

6061
type PythonCodeDetection =
61-
| { isPython: true; pythonOriginalCode: string }
62+
| {
63+
isPython: true;
64+
pythonOriginalCode: string;
65+
options: YamlGeneratorOptions;
66+
}
6267
| { isPython: false };
6368

6469
export const ComponentEditorDialog = withSuspenseWrapper(
@@ -86,7 +91,14 @@ export const ComponentEditorDialog = withSuspenseWrapper(
8691
text,
8792
});
8893

89-
const pythonOriginalCode = hydratedComponent?.spec?.metadata
94+
if (
95+
!hydratedComponent ||
96+
!isContainerImplementation(hydratedComponent.spec.implementation)
97+
) {
98+
return { isPython: false };
99+
}
100+
101+
const pythonOriginalCode = hydratedComponent.spec.metadata
90102
?.annotations?.python_original_code as string;
91103

92104
if (!pythonOriginalCode) {
@@ -96,6 +108,25 @@ export const ComponentEditorDialog = withSuspenseWrapper(
96108
return {
97109
isPython: true,
98110
pythonOriginalCode,
111+
options: {
112+
baseImage:
113+
hydratedComponent.spec.implementation.container.image ??
114+
"python:3.12",
115+
packagesToInstall: parsePythonDependencies(
116+
hydratedComponent.spec.metadata?.annotations
117+
?.python_dependencies,
118+
),
119+
annotations: Object.fromEntries(
120+
Object.entries(
121+
(hydratedComponent.spec.metadata?.annotations ??
122+
{}) as Record<string, string>,
123+
).filter(
124+
([key]) =>
125+
key !== "python_original_code" &&
126+
key !== "python_dependencies",
127+
),
128+
),
129+
},
99130
};
100131
}
101132

@@ -110,6 +141,7 @@ export const ComponentEditorDialog = withSuspenseWrapper(
110141
return {
111142
isPython: true,
112143
pythonOriginalCode: defaultPythonFunctionText,
144+
options: {},
113145
};
114146
},
115147
});
@@ -184,6 +216,7 @@ export const ComponentEditorDialog = withSuspenseWrapper(
184216
{pythonCodeDetection?.isPython ? (
185217
<PythonComponentEditor
186218
text={pythonCodeDetection.pythonOriginalCode}
219+
options={pythonCodeDetection.options}
187220
onComponentTextChange={handleComponentTextChange}
188221
onErrorsChange={setErrors}
189222
/>
@@ -200,3 +233,13 @@ export const ComponentEditorDialog = withSuspenseWrapper(
200233
},
201234
ComponentEditorDialogSkeleton,
202235
);
236+
237+
function parsePythonDependencies(dependencies: unknown | undefined): string[] {
238+
try {
239+
return (
240+
JSON.parse(typeof dependencies === "string" ? dependencies : "[]") ?? []
241+
);
242+
} catch {
243+
return [];
244+
}
245+
}

src/components/shared/ComponentEditor/components/PythonComponentEditor.tsx

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { useCallback, useEffect, useState } from "react";
44
import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper";
55
import { BlockStack, InlineStack } from "@/components/ui/layout";
66
import { Skeleton } from "@/components/ui/skeleton";
7-
import { Text } from "@/components/ui/typography";
7+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
88

99
import { DEFAULT_MONACO_OPTIONS } from "../constants";
1010
import { usePythonYamlGenerator } from "../generators/python";
11+
import type { YamlGeneratorOptions } from "../types";
1112
import { ComponentSpecErrorsList } from "./ComponentSpecErrorsList";
1213
import { PreviewTaskNodeCard } from "./PreviewTaskNodeCard";
1314
import { TogglePreview } from "./TogglePreview";
15+
import { YamlGeneratorOptionsEditor } from "./YamlGeneratorOptionsEditor";
1416

1517
const PythonComponentEditorSkeleton = () => {
1618
return (
@@ -43,22 +45,26 @@ const PythonComponentEditorSkeleton = () => {
4345
export const PythonComponentEditor = withSuspenseWrapper(
4446
({
4547
text,
48+
options,
4649
onComponentTextChange,
4750
onErrorsChange,
4851
}: {
4952
text: string;
53+
options: YamlGeneratorOptions;
5054
onComponentTextChange: (yaml: string) => void;
5155
onErrorsChange: (errors: string[]) => void;
5256
}) => {
5357
const [componentText, setComponentText] = useState("");
5458
const [validationErrors, setValidationErrors] = useState<string[]>([]);
5559
const [showPreview, setShowPreview] = useState(true);
60+
const [yamlGeneratorOptions, setYamlGeneratorOptions] = useState(options);
61+
5662
const yamlGenerator = usePythonYamlGenerator();
5763

5864
const handleFunctionTextChange = useCallback(
5965
async (value: string | undefined) => {
6066
try {
61-
const yaml = await yamlGenerator(value ?? "");
67+
const yaml = await yamlGenerator(value ?? "", yamlGeneratorOptions);
6268
setComponentText(yaml);
6369
onComponentTextChange(yaml);
6470
setValidationErrors([]);
@@ -71,7 +77,7 @@ export const PythonComponentEditor = withSuspenseWrapper(
7177
setValidationErrors(errors);
7278
}
7379
},
74-
[yamlGenerator, onComponentTextChange],
80+
[yamlGenerator, onComponentTextChange, yamlGeneratorOptions],
7581
);
7682

7783
useEffect(() => {
@@ -82,21 +88,40 @@ export const PythonComponentEditor = withSuspenseWrapper(
8288
return (
8389
<InlineStack className="w-full h-full" gap="4">
8490
<BlockStack className="flex-1 h-full" data-testid="python-editor">
85-
<InlineStack className="h-10 py-2">
86-
<Text>Python Code</Text>
87-
</InlineStack>
88-
<BlockStack className="flex-1 relative">
89-
<div className="absolute inset-0">
90-
<MonacoEditor
91-
defaultLanguage="python"
92-
theme="vs-dark"
93-
value={text}
94-
onChange={handleFunctionTextChange}
95-
options={DEFAULT_MONACO_OPTIONS}
91+
<Tabs defaultValue="python" className="w-full flex-1 pt-1">
92+
<TabsList>
93+
<TabsTrigger value="python">Python Code</TabsTrigger>
94+
<TabsTrigger value="metadata">Configuration</TabsTrigger>
95+
</TabsList>
96+
97+
<TabsContent
98+
value="python"
99+
className="flex flex-col flex-1 relative"
100+
>
101+
<BlockStack className="flex-1 relative">
102+
<div className="absolute inset-0">
103+
<MonacoEditor
104+
defaultLanguage="python"
105+
theme="vs-dark"
106+
value={text}
107+
onChange={handleFunctionTextChange}
108+
options={DEFAULT_MONACO_OPTIONS}
109+
/>
110+
</div>
111+
</BlockStack>
112+
<ComponentSpecErrorsList validationErrors={validationErrors} />
113+
</TabsContent>
114+
115+
<TabsContent
116+
value="metadata"
117+
className="flex flex-col flex-1 relative"
118+
>
119+
<YamlGeneratorOptionsEditor
120+
initialOptions={yamlGeneratorOptions}
121+
onChange={setYamlGeneratorOptions}
96122
/>
97-
</div>
98-
</BlockStack>
99-
<ComponentSpecErrorsList validationErrors={validationErrors} />
123+
</TabsContent>
124+
</Tabs>
100125
</BlockStack>
101126

102127
<BlockStack className="flex-1 h-full">

0 commit comments

Comments
 (0)