From c629823207e3e154a6c4e9d64f1ec13c67de80d3 Mon Sep 17 00:00:00 2001 From: Lily Delalande <119957291+lily-de@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:45:00 -0400 Subject: [PATCH] ui: add timeout field to settings v2 modal (#1955) --- ui/desktop/src/components/Modal.tsx | 12 +++-- .../extensions/modal/ExtensionModal.tsx | 42 +++++++++++++-- .../modal/ExtensionTimeoutField.tsx | 54 +++++++++++++++++++ .../settings_v2/extensions/utils.ts | 8 +++ 4 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx diff --git a/ui/desktop/src/components/Modal.tsx b/ui/desktop/src/components/Modal.tsx index 2521188c..0f59b097 100644 --- a/ui/desktop/src/components/Modal.tsx +++ b/ui/desktop/src/components/Modal.tsx @@ -46,23 +46,29 @@ export default function Modal({ } }; - // Add event listener + // Add event listener for Escape key document.addEventListener('keydown', handleEscKey); + // Add overflow-hidden to body to prevent scrolling background + document.body.style.overflow = 'hidden'; + // Clean up return () => { document.removeEventListener('keydown', handleEscKey); + // Restore body scrolling when modal closes + document.body.style.overflow = ''; }; }, [onClose]); return (
{children}
{footer && ( diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx index 1715de5a..fd03b123 100644 --- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx @@ -6,6 +6,7 @@ import EnvVarsSection from './EnvVarsSection'; import ExtensionConfigFields from './ExtensionConfigFields'; import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react'; import ExtensionInfoFields from './ExtensionInfoFields'; +import ExtensionTimeoutField from './ExtensionTimeoutField'; interface ExtensionModalProps { title: string; @@ -84,9 +85,23 @@ export default function ExtensionModal({ ); }; + const isTimeoutValid = () => { + // Check if timeout is not undefined, null, or empty string + if (formData.timeout === undefined || formData.timeout === null) { + return false; + } + + // Convert to number if it's a string + const timeoutValue = + typeof formData.timeout === 'string' ? Number(formData.timeout) : formData.timeout; + + // Check if it's a valid number (not NaN) and is a positive number + return !isNaN(timeoutValue) && timeoutValue > 0; + }; + // Form validation const isFormValid = () => { - return isNameValid() && isConfigValid() && isEnvVarsValid(); + return isNameValid() && isConfigValid() && isEnvVarsValid() && isTimeoutValid(); }; // Handle submit with validation @@ -94,9 +109,20 @@ export default function ExtensionModal({ setSubmitAttempted(true); if (isFormValid()) { - onSubmit(formData); + const dataToSubmit = { ...formData }; + + // Convert the timeout to a number if it's a string + if (typeof dataToSubmit.timeout === 'string') { + dataToSubmit.timeout = Number(dataToSubmit.timeout); + } + + // Submit the data with converted timeout + onSubmit(dataToSubmit); + onClose(); // Only close the modal if the form is valid + } else { + // Optional: Add some feedback that validation failed (like a toast notification) + console.log('Form validation failed'); } - onClose(); }; // Create footer buttons based on current state @@ -186,7 +212,7 @@ export default function ExtensionModal({ /> {/* Divider */} -
+
{/* Command */}
@@ -198,10 +224,16 @@ export default function ExtensionModal({ submitAttempted={submitAttempted} isValid={isConfigValid()} /> +
+ setFormData({ ...formData, [key]: value })} + submitAttempted={submitAttempted} + />
{/* Divider */} -
+
{/* Environment Variables */}
diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx new file mode 100644 index 00000000..4f33fe66 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx @@ -0,0 +1,54 @@ +import { Input } from '../../../ui/input'; +import Select from 'react-select'; +import React, { useState } from 'react'; + +interface ExtensionTimeoutFieldProps { + timeout: number; + onChange: (key: string, value: any) => void; + submitAttempted: boolean; +} + +export default function ExtensionTimeoutField({ + timeout, + onChange, + submitAttempted, +}: ExtensionTimeoutFieldProps) { + const isTimeoutValid = () => { + // Check if timeout is not undefined, null, or empty string + if (timeout === undefined || timeout === null) { + return false; + } + + // Convert to number if it's a string + const timeoutValue = typeof timeout === 'string' ? Number(timeout) : timeout; + + // Check if it's a valid number (not NaN) and is a positive number + return !isNaN(timeoutValue) && timeoutValue > 0; + }; + + return ( +
+ {/* Row with Timeout and timeout input side by side */} +
+
+ +
+ + {/* Type Dropdown */} +
+
+ onChange('timeout', e.target.value)} + defaultValue={300} + className={`${!submitAttempted || isTimeoutValid() ? 'border-borderSubtle' : 'border-red-500'} text-textStandard focus:border-borderStandard`} + /> + {submitAttempted && !isTimeoutValid() && ( +
Timeout
+ )} +
+
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings_v2/extensions/utils.ts index abc3006a..dc7989b7 100644 --- a/ui/desktop/src/components/settings_v2/extensions/utils.ts +++ b/ui/desktop/src/components/settings_v2/extensions/utils.ts @@ -1,5 +1,7 @@ // Default extension timeout in seconds // TODO: keep in sync with rust better +import * as module from 'node:module'; + export const DEFAULT_EXTENSION_TIMEOUT = 300; /** @@ -24,6 +26,7 @@ export interface ExtensionFormData { cmd?: string; endpoint?: string; enabled: boolean; + timeout?: number; envVars: { key: string; value: string }[]; } @@ -35,6 +38,7 @@ export function getDefaultFormData(): ExtensionFormData { cmd: '', endpoint: '', enabled: true, + timeout: 300, envVars: [], }; } @@ -59,6 +63,7 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, endpoint: extension.type === 'sse' ? extension.uri : undefined, enabled: extension.enabled, + timeout: extension.timeout, envVars, }; } @@ -84,6 +89,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon description: formData.description, cmd: cmd, args: args, + timeout: formData.timeout, ...(Object.keys(envs).length > 0 ? { envs } : {}), }; } else if (formData.type === 'sse') { @@ -91,6 +97,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon type: 'sse', name: formData.name, description: formData.description, + timeout: formData.timeout, uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type ...(Object.keys(envs).length > 0 ? { envs } : {}), }; @@ -99,6 +106,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon return { type: formData.type, name: formData.name, + timeout: formData.timeout, }; } }