app/components/ConfigurationPanel.tsx (676 lines of code) (raw):

import { useState, useEffect } from "react"; import { ConfigService, DockerConfig, DEFAULT_TEMPLATE, } from "~/lib/configService"; import { AIProviderIcon, OpenAIIcon, ClaudeIcon, } from "~/components/AIProviderIcons"; import { useAutoSave } from "~/hooks/useAutoSave"; import { AutoSaveService, STORAGE_KEYS, getFromLocalStorage, setToLocalStorage, } from "~/lib/autoSaveService"; // Known environments with predefined configurations export const KNOWN_ENVIRONMENTS = [ { id: "hugex-codex", name: "hugex Codex (Recommended)", description: "Latest environment with enhanced AI coding capabilities", image: "drbh/codex-universal-explore:dev", tags: ["AI", "OpenAI", "Codex", "Coding", "Latest"], requiredSecrets: ["OPENAI_API_KEY"], defaultEnvironment: { LLM_MODEL: "gpt-4o", // LLM_PROVIDER: "openai", // CODEX_MODE: "enhanced", }, icon: "openai", notes: { info: "Latest Codex Universal image with enhanced AI capabilities and improved code generation. Requires OpenAI API access.", link: "https://github.com/drbh/codex-universal", }, }, // { // id: "hugex-chatgpt", // name: "hugex ChatGPT (Legacy)", // description: "Legacy environment for OpenAI ChatGPT models", // image: "drbh/codex-universal-explore:alpha", // tags: ["AI", "OpenAI", "ChatGPT", "Coding", "Legacy"], // requiredSecrets: ["OPENAI_API_KEY"], // defaultEnvironment: { // LLM_MODEL: "gpt-4o", // LLM_PROVIDER: "openai", // }, // icon: "openai", // notes: { // info: "Legacy version of the Codex Universal image. Consider upgrading to the latest Codex environment for better performance.", // link: "https://github.com/drbh/codex-universal", // }, // }, // { // id: "hugex-claude", // name: "hugex Claude", // description: "Environment optimized for Anthropic Claude models", // image: "drbh/codex-universal-explore:alpha", // tags: ["AI", "Anthropic", "Claude", "Coding"], // requiredSecrets: ["ANTHROPIC_API_KEY"], // defaultEnvironment: { // LLM_MODEL: "claude-3-7-sonnet-20250219", // LLM_PROVIDER: "anthropic", // }, // icon: "claude", // notes: { // info: "This image is a custom version of the Codex Universal image, source available at drbh/codex-universal. It includes small modifications to support hugex", // link: "https://github.com/drbh/codex-universal", // }, // }, // { // id: "custom", // name: "Custom Environment", // description: "Configure your own Docker environment", // image: "ubuntu:latest", // tags: ["Custom", "Docker", "Flexible"], // requiredSecrets: [], // defaultEnvironment: {}, // icon: "fas fa-cog", // notes: { // info: "Make sure the container expects PROMPT, REPO_URL and REPO_BRANCH environment variables to be set as well as exposing the git diff to the container logs to be compatible with hugex", // }, // }, ]; interface ConfigurationPanelProps { onConfigChange?: () => void; } export const ConfigurationPanel = ({ onConfigChange, }: ConfigurationPanelProps) => { const [selectedEnvironment, setSelectedEnvironment] = useState( KNOWN_ENVIRONMENTS[0].id ); const [dockerConfig, setDockerConfig] = useState<DockerConfig>({ image: "drbh/codex-universal-explore:dev", environment: {}, secrets: {}, }); const [isExpanded, setIsExpanded] = useState(true); const [newEnvKey, setNewEnvKey] = useState(""); const [newEnvValue, setNewEnvValue] = useState(""); const [newSecretKey, setNewSecretKey] = useState(""); const [newSecretValue, setNewSecretValue] = useState(""); const [templateText, setTemplateText] = useState(DEFAULT_TEMPLATE); // Auto-save configuration changes const dockerAutoSave = useAutoSave([dockerConfig], { delay: 500, // Much faster - 500ms onSave: async () => { await ConfigService.updateDockerConfig(dockerConfig); onConfigChange?.(); }, onSuccess: () => { // No intrusive success message, just console log console.log("Configuration auto-saved successfully"); }, onError: (error) => { // Only show errors, not success AutoSaveService.showSaveIndicator( "Failed to save configuration", "error" ); console.error("Auto-save failed:", error); }, enabled: true, }); // Load configuration from localStorage const loadLocalConfiguration = () => { try { const savedConfig = getFromLocalStorage(STORAGE_KEYS.dockerConfig, null); if (savedConfig) { setDockerConfig(savedConfig); // Detect which environment matches the current docker image const matchingEnv = KNOWN_ENVIRONMENTS.find( (env) => env.image === savedConfig.image ); if (matchingEnv) { setSelectedEnvironment(matchingEnv.id); } else { setSelectedEnvironment("custom"); } } const savedTemplateText = getFromLocalStorage( STORAGE_KEYS.templateText, DEFAULT_TEMPLATE ); setTemplateText(savedTemplateText); } catch (error) { console.error("Failed to load local configuration:", error); } }; // Save configuration to localStorage with auto-save const saveLocalConfiguration = (config: DockerConfig) => { setToLocalStorage(STORAGE_KEYS.dockerConfig, config, true); }; useEffect(() => { // First load from localStorage for immediate UX loadLocalConfiguration(); // Then load from server for sync loadConfiguration(); }, []); const loadConfiguration = async () => { try { const dockerData = await ConfigService.getDockerConfig(); setDockerConfig(dockerData); // Detect which environment matches the current docker image const matchingEnv = KNOWN_ENVIRONMENTS.find( (env) => env.image === dockerData.image ); if (matchingEnv) { setSelectedEnvironment(matchingEnv.id); } else { // If no match found, default to custom setSelectedEnvironment("custom"); } // Save to localStorage for persistence saveLocalConfiguration(dockerData); } catch (error) { console.error("Failed to load configuration:", error); } }; const validateGitUrl = (url: string): boolean => { // This function is deprecated but kept for potential future use const gitUrlPattern = /^https:\/\/github\.com\/[\w\-\.]+\/[\w\-\.]+(?:\.git)?(?:\/)?$/; return gitUrlPattern.test(url); }; const handleRepoUrlChange = (url: string) => { // Repository configuration is now handled per-job in the create task form console.warn("Repository URL change ignored - deprecated functionality"); }; const handleSaveRepoConfig = async () => { // Repository configuration is now handled per-job in the create task form console.warn("Repository save ignored - deprecated functionality"); }; const handleExecutionModeChange = async (newMode: string) => { // Execution mode is now always 'api' - Docker mode has been deprecated console.warn("Execution mode change ignored - always using 'api' mode"); }; const showSuccessMessage = (message: string, bgColor = "bg-green-500") => { const successMessage = document.createElement("div"); successMessage.className = `fixed top-4 right-4 ${bgColor} text-white px-4 py-2 rounded-lg shadow-lg z-50`; successMessage.textContent = message; document.body.appendChild(successMessage); setTimeout(() => { document.body.removeChild(successMessage); }, 3000); }; const addEnvironmentVariable = () => { if (newEnvKey.trim() && newEnvValue.trim()) { const newConfig = { ...dockerConfig, environment: { ...dockerConfig.environment, [newEnvKey.trim()]: newEnvValue.trim(), }, }; setDockerConfig(newConfig); saveLocalConfiguration(newConfig); setNewEnvKey(""); setNewEnvValue(""); } }; const removeEnvironmentVariable = (key: string) => { const newConfig = { ...dockerConfig, environment: { ...dockerConfig.environment }, }; delete newConfig.environment[key]; setDockerConfig(newConfig); saveLocalConfiguration(newConfig); }; const addSecret = () => { if (newSecretKey.trim() && newSecretValue.trim()) { const newConfig = { ...dockerConfig, secrets: { ...dockerConfig.secrets, [newSecretKey.trim()]: newSecretValue.trim(), }, }; setDockerConfig(newConfig); saveLocalConfiguration(newConfig); setNewSecretKey(""); setNewSecretValue(""); } }; const removeSecret = (key: string) => { const newConfig = { ...dockerConfig, secrets: { ...dockerConfig.secrets }, }; delete newConfig.secrets[key]; setDockerConfig(newConfig); saveLocalConfiguration(newConfig); }; const handleEnvironmentSelect = (envId: string) => { const env = KNOWN_ENVIRONMENTS.find((e) => e.id === envId); if (env) { setSelectedEnvironment(envId); const newConfig = { ...dockerConfig, image: env.image, environment: { ...dockerConfig.environment, ...env.defaultEnvironment }, }; setDockerConfig(newConfig); saveLocalConfiguration(newConfig); } }; return ( <div className="p-6"> <div className="space-y-6"> {/* Environment Selection */} <div> <h2 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100"> Select Environment </h2> <div className="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2"> {KNOWN_ENVIRONMENTS.map((env) => { const isSelected = selectedEnvironment === env.id; const hasRequiredSecrets = env.requiredSecrets.every( (secret) => dockerConfig.secrets[secret] ); return ( <div key={env.id} onClick={() => handleEnvironmentSelect(env.id)} className={`cursor-pointer rounded-lg border p-4 transition-all hover:shadow-md ${ isSelected ? "border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20" : "border-gray-200 hover:border-gray-300 dark:border-gray-600 dark:hover:border-gray-500" }`} > <div className="flex items-start gap-3"> <div className={`rounded-lg p-2 ${ isSelected ? "bg-blue-100 dark:bg-blue-800" : "bg-gray-100 dark:bg-gray-700" }`} > {env.icon === "openai" ? ( <OpenAIIcon className={ isSelected ? "text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-300" } size={20} /> ) : env.icon === "claude" ? ( <ClaudeIcon className={ isSelected ? "text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-300" } size={20} /> ) : ( <i className={`${env.icon} text-lg ${ isSelected ? "text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-300" }`} ></i> )} </div> <div className="flex-1"> <div className="mb-2 flex items-center justify-between"> <h3 className={`flex items-center gap-2 font-medium ${ isSelected ? "text-blue-900 dark:text-blue-100" : "text-gray-900 dark:text-gray-100" }`} > {env.name} {env.id === "hugex-codex" && ( <span className="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/30 dark:text-green-300"> <i className="fas fa-star mr-1 text-xs"></i> Recommended </span> )} {env.tags.includes("Legacy") && ( <span className="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"> Legacy </span> )} </h3> {isSelected && ( <div className="flex items-center gap-1 text-blue-600 dark:text-blue-400"> <i className="fas fa-check-circle text-sm"></i> <span className="text-xs font-medium"> Selected </span> </div> )} </div> <p className="mb-3 text-sm text-gray-600 dark:text-gray-400"> {env.description} </p> {/* Tags */} <div className="mb-2 flex flex-wrap gap-1"> {env.tags.map((tag) => { const aiIcon = AIProviderIcon({ tags: [tag], size: 12, }); return ( <span key={tag} className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs ${ isSelected ? "bg-blue-100 text-blue-700 dark:bg-blue-800 dark:text-blue-300" : "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300" }`} > {aiIcon && ( <span className="flex items-center"> {aiIcon} </span> )} {tag} </span> ); })} </div> {/* Required secrets indicator */} {env.requiredSecrets.length > 0 && ( <div className={`flex items-center gap-1 text-xs ${ hasRequiredSecrets ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-400" }`} > <i className={`fas ${ hasRequiredSecrets ? "fa-check-circle" : "fa-exclamation-triangle" }`} ></i> <span> {hasRequiredSecrets ? "All required secrets configured" : `Requires: ${env.requiredSecrets.join(", ")}`} </span> </div> )} </div> </div> </div> ); })} </div> </div> <hr className="mb-6 border-gray-200 dark:border-gray-700" /> <div className="mb-4 flex items-center justify-between"> <div className="flex items-center gap-3"> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> Details </h2> {(() => { const currentEnv = KNOWN_ENVIRONMENTS.find( (env) => env.id === selectedEnvironment ); if (currentEnv && currentEnv.requiredSecrets.length > 0) { const missingSecrets = currentEnv.requiredSecrets.filter( (secret) => !dockerConfig.secrets[secret] ); if (missingSecrets.length > 0) { return ( <div className="flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 px-2 py-1 dark:border-amber-800 dark:bg-amber-900/20"> <i className="fas fa-exclamation-triangle text-xs text-amber-600 dark:text-amber-400"></i> <span className="text-xs font-medium text-amber-700 dark:text-amber-300"> {missingSecrets.length} missing </span> </div> ); } } // Check if Docker image is empty or default if ( !dockerConfig.image || dockerConfig.image === "ubuntu:latest" ) { return ( <div className="flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 px-2 py-1 dark:border-blue-800 dark:bg-blue-900/20"> <i className="fas fa-info-circle text-xs text-blue-600 dark:text-blue-400"></i> <span className="text-xs font-medium text-blue-700 dark:text-blue-300"> Needs setup </span> </div> ); } return ( <div className="flex items-center gap-1 rounded-md border border-green-200 bg-green-50 px-2 py-1 dark:border-green-800 dark:bg-green-900/20"> <i className="fas fa-check-circle text-xs text-green-600 dark:text-green-400"></i> <span className="text-xs font-medium text-green-700 dark:text-green-300"> Ready </span> </div> ); })()} </div> <button onClick={() => setIsExpanded(!isExpanded)} className="text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100" > {isExpanded ? ( <i className="fas fa-chevron-up"></i> ) : ( <i className="fas fa-chevron-down"></i> )} </button> </div> {/* Docker Configuration */} <div className={`space-y-6 ${isExpanded ? "block" : "hidden"}`}> {/* Docker Image */} <div> <label htmlFor="docker-image" className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" > Docker Image </label> <div className="flex items-center gap-2"> <input type="text" id="docker-image" value={dockerConfig.image} onChange={(e) => { const newImage = e.target.value; setDockerConfig((prev) => ({ ...prev, image: newImage, })); // Update selected environment when image changes const matchingEnv = KNOWN_ENVIRONMENTS.find( (env) => env.image === newImage ); if (matchingEnv) { setSelectedEnvironment(matchingEnv.id); } else { setSelectedEnvironment("custom"); } }} className="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" placeholder="docker/image:tag" /> {/* Subtle auto-save status indicator positioned better */} <div className="flex h-6 w-6 items-center justify-center"> {dockerAutoSave.isAutoSaving && ( <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-blue-400" title="Saving..." ></div> )} {dockerAutoSave.hasUnsavedChanges && !dockerAutoSave.isAutoSaving ? ( <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" title="Pending save..." ></div> ) : ( !dockerAutoSave.isAutoSaving && dockerAutoSave.lastSaved && ( <div className="h-1.5 w-1.5 rounded-full bg-green-400 opacity-50" title="Saved" ></div> ) )} </div> </div> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> Specify the Docker image to use for job execution </p> {/* Show environment-specific notes */} {(() => { const currentEnv = KNOWN_ENVIRONMENTS.find( (env) => env.id === selectedEnvironment ); if ( currentEnv && currentEnv.notes && dockerConfig.image === currentEnv.image ) { return ( <p className="mt-2 text-xs text-yellow-600 dark:text-yellow-400"> Note: {currentEnv.notes.info} {currentEnv.notes.link && ( <> {" "} <a href={currentEnv.notes.link} className="text-blue-600 hover:underline dark:text-blue-400" target="_blank" rel="noopener noreferrer" > {currentEnv.notes.link.replace( "https://github.com/", "" )} </a> </> )} </p> ); } return null; })()} </div> {/* Prompt Template */} <div className="mt-6"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> Prompt Template </label> <input type="text" value={templateText} onChange={(e) => { setTemplateText(e.target.value); setToLocalStorage( STORAGE_KEYS.templateText, e.target.value, true ); onConfigChange?.(); // Notify parent of template change }} placeholder="Enter instruction to append to all prompts (leave empty to disable)" className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" /> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> This instruction will be automatically appended to all task prompts when not empty </p> </div> {/* Environment Variables */} <div className="mt-6"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> Environment Variables </label> <div className="space-y-2"> {Object.entries(dockerConfig.environment).map(([key, value]) => ( <div key={key} className="flex items-center gap-2"> <input type="text" value={key} disabled className="w-1/3 rounded-md border border-gray-300 bg-gray-100 px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-100" /> <input type="text" value={value} disabled className="flex-1 rounded-md border border-gray-300 bg-gray-100 px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-100" /> <button onClick={() => removeEnvironmentVariable(key)} className="rounded-md border border-red-300 px-3 py-2 text-red-300 transition-colors hover:bg-red-50 dark:border-red-800 dark:text-red-900 dark:hover:bg-gray-800" > <i className="fas fa-trash"></i> </button> </div> ))} <div className="flex items-center gap-2"> <input type="text" value={newEnvKey} onChange={(e) => setNewEnvKey(e.target.value)} placeholder="KEY" className="w-1/3 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" /> <input type="text" value={newEnvValue} onChange={(e) => setNewEnvValue(e.target.value)} placeholder="Value" className="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" /> <button onClick={addEnvironmentVariable} disabled={!newEnvKey.trim() || !newEnvValue.trim()} className="rounded-md border border-gray-300 px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800" > <i className="fas fa-plus"></i> </button> </div> </div> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> Add custom environment variables for the Docker container </p> </div> {/* Secrets */} <div className="mt-6"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> Secrets </label> {/* Quick Add Common Secrets */} {/* <div className="mb-3 flex flex-wrap gap-2"> {[ { key: 'OPENAI_API_KEY', placeholder: 'sk-...', icon: 'fas fa-brain' }, { key: 'ANTHROPIC_API_KEY', placeholder: 'sk-ant-...', icon: 'fas fa-robot' }, { key: 'GITHUB_TOKEN', placeholder: 'ghp_...', icon: 'fab fa-github' }, ].map(({ key, placeholder, icon }) => ( !dockerConfig.secrets[key] && ( <button key={key} onClick={() => { setNewSecretKey(key); // Focus the value input setTimeout(() => { const valueInput = document.querySelector('input[placeholder="Secret value"]') as HTMLInputElement; if (valueInput) valueInput.focus(); }, 0); }} className="inline-flex items-center gap-2 px-3 py-1.5 text-xs bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-700 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors" > <i className={`${icon} text-xs`}></i> Add {key.replace('_', ' ')} </button> ) ))} </div> */} <div className="space-y-2"> {Object.entries(dockerConfig.secrets).map(([key, value]) => ( <div key={key} className="flex items-center gap-2"> <input type="text" value={key} disabled className="w-1/3 rounded-md border border-gray-300 bg-gray-100 px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-100" /> <input type="password" value="••••••••" disabled className="flex-1 rounded-md border border-gray-300 bg-gray-100 px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-100" /> <button onClick={() => removeSecret(key)} className="rounded-md border border-red-300 px-3 py-2 text-red-300 transition-colors hover:bg-red-50 dark:border-red-800 dark:text-red-900 dark:hover:bg-gray-800" > <i className="fas fa-trash"></i> </button> </div> ))} <div className="flex items-center gap-2"> <input type="text" value={newSecretKey} onChange={(e) => setNewSecretKey(e.target.value)} placeholder={(() => { const currentEnv = KNOWN_ENVIRONMENTS.find( (env) => env.id === selectedEnvironment ); if (currentEnv && currentEnv.requiredSecrets.length > 0) { const missingRequired = currentEnv.requiredSecrets.find( (secret) => !dockerConfig.secrets[secret] ); if (missingRequired) { return missingRequired; } } return "SECRET_NAME"; })()} className="w-1/3 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" /> <input type="password" value={newSecretValue} onChange={(e) => setNewSecretValue(e.target.value)} placeholder="Secret value" className="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" /> <button onClick={addSecret} disabled={!newSecretKey.trim() || !newSecretValue.trim()} className="rounded-md border border-gray-300 px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800" > <i className="fas fa-plus"></i> </button> </div> </div> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> Add sensitive configuration values as secrets </p> {(() => { const currentEnv = KNOWN_ENVIRONMENTS.find( (env) => env.id === selectedEnvironment ); if (currentEnv && currentEnv.requiredSecrets.length > 0) { const missingSecrets = currentEnv.requiredSecrets.filter( (secret) => !dockerConfig.secrets[secret] ); if (missingSecrets.length > 0) { return ( <p className="mt-2 flex items-center gap-1 text-xs text-red-600 dark:text-red-400"> <i className="fas fa-exclamation-triangle"></i> {missingSecrets.join(", ")} required for {currentEnv.name} </p> ); } } return null; })()} </div> </div> </div> </div> ); };