diff --git a/site/src/pages/TemplateBuilder/ModuleConfiguration.stories.tsx b/site/src/pages/TemplateBuilder/ModuleConfiguration.stories.tsx index 78b77f9a8639c..74ca2c63d4b0b 100644 --- a/site/src/pages/TemplateBuilder/ModuleConfiguration.stories.tsx +++ b/site/src/pages/TemplateBuilder/ModuleConfiguration.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, within } from "storybook/test"; import type { FormHelpers } from "#/utils/formUtils"; import { ModuleConfiguration } from "./ModuleConfiguration"; @@ -113,3 +113,64 @@ export const WithoutIcon: Story = { ], }, }; + +export const WithSensitiveVariables: Story = { + args: { + name: "Claude Code", + description: "Run the Claude Code agent in your workspace.", + iconUrl: "/icon/claude.svg", + detailsUrl: "https://registry.coder.com/modules/claude-code", + optionalFields: [ + { + type: "select", + id: "model", + label: "Model", + options: [ + { value: "sonnet", label: "Sonnet" }, + { value: "opus", label: "Opus" }, + ], + }, + ], + sensitiveVariables: [ + { + name: "claude_code_oauth_token", + type: "string", + description: "OAuth token used by Claude Code", + required: true, + sensitive: true, + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const note = await canvas.findByTestId("module-sensitive-variables"); + await expect(note).toBeVisible(); + await expect(note).toHaveTextContent("claude_code_oauth_token"); + }, +}; + +export const NoConfigWithSensitiveVariables: Story = { + args: { + name: "OpenAI Codex", + description: "Install the OpenAI Codex CLI in your workspace.", + iconUrl: "/icon/openai.svg", + detailsUrl: "https://registry.coder.com/modules/codex", + sensitiveVariables: [ + { + name: "openai_api_key", + type: "string", + description: "OpenAI API key", + required: true, + sensitive: true, + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // The sensitive-vars note renders even when the module has no + // configurable fields (the "No configuration required." branch). + await expect(canvas.getByText("No configuration required.")).toBeVisible(); + const note = await canvas.findByTestId("module-sensitive-variables"); + await expect(note).toHaveTextContent("openai_api_key"); + }, +}; diff --git a/site/src/pages/TemplateBuilder/ModuleConfiguration.tsx b/site/src/pages/TemplateBuilder/ModuleConfiguration.tsx index 85de9a946cdb6..3c8c4081b46fa 100644 --- a/site/src/pages/TemplateBuilder/ModuleConfiguration.tsx +++ b/site/src/pages/TemplateBuilder/ModuleConfiguration.tsx @@ -1,4 +1,5 @@ -import { CheckIcon, TrashIcon } from "lucide-react"; +import { CheckIcon, InfoIcon, TrashIcon } from "lucide-react"; +import type { TemplateBuilderModuleVariable } from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; import { CollapsibleSummary } from "#/components/CollapsibleSummary/CollapsibleSummary"; import { TemplateBuilderAvatarData } from "#/pages/TemplateBuilder/TemplateBuilderAvatarData"; @@ -16,6 +17,13 @@ type ModuleConfigurationProps = { onRemove?: () => void; fields?: ConfigurationFieldDefinition[]; optionalFields?: ConfigurationFieldDefinition[]; + /** + * Sensitive variables belonging to this module. Rendered as an info note + * at the bottom of the module's grey card because their values are + * collected from the developer at workspace creation, not during template + * composition. + */ + sensitiveVariables?: TemplateBuilderModuleVariable[]; }; export const ModuleConfiguration: React.FC = ({ @@ -26,6 +34,7 @@ export const ModuleConfiguration: React.FC = ({ onRemove, fields, optionalFields, + sensitiveVariables, }) => { return (
@@ -72,6 +81,26 @@ export const ModuleConfiguration: React.FC = ({ No configuration required. )} + + {sensitiveVariables && sensitiveVariables.length > 0 && ( +
+ +

+ {sensitiveVariables.map((v) => ( + + {v.name} + + ))} + will be collected from developers at workspace creation. +

+
+ )}
); }; diff --git a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx index ed844dd1d8bd3..3b54ce86917d8 100644 --- a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx +++ b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx @@ -1,4 +1,3 @@ -import { InfoIcon } from "lucide-react"; import type { FC } from "react"; import { useQuery } from "react-query"; import { templateBuilderModules } from "#/api/queries/templateBuilder"; @@ -164,25 +163,9 @@ export const ModuleSettingsStep: FC = ({ detailsUrl={moduleDetailsUrl(mod.id)} fields={requiredFields} optionalFields={optionalFields} + sensitiveVariables={sensitiveVars} onRemove={() => onRemoveModule(mod.id)} /> - - {sensitiveVars.length > 0 && ( -
- -

- {sensitiveVars.map((v) => ( - - {v.name} - - ))} - will be collected from developers at workspace creation. -

-
- )} ); })}