ui: add timeout field to settings v2 modal (#1955)

This commit is contained in:
Lily Delalande
2025-03-31 21:45:00 -04:00
committed by GitHub
parent e2390935a6
commit c629823207
4 changed files with 108 additions and 8 deletions
+9 -3
View File
@@ -46,23 +46,29 @@ export default function Modal({
} }
}; };
// Add event listener // Add event listener for Escape key
document.addEventListener('keydown', handleEscKey); document.addEventListener('keydown', handleEscKey);
// Add overflow-hidden to body to prevent scrolling background
document.body.style.overflow = 'hidden';
// Clean up // Clean up
return () => { return () => {
document.removeEventListener('keydown', handleEscKey); document.removeEventListener('keydown', handleEscKey);
// Restore body scrolling when modal closes
document.body.style.overflow = '';
}; };
}, [onClose]); }, [onClose]);
return ( return (
<div <div
className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards] flex items-center justify-center p-4" className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards] flex items-center justify-center p-4 z-[9999]"
onClick={handleBackdropClick} onClick={handleBackdropClick}
style={{ isolation: 'isolate' }} /* Creates a new stacking context */
> >
<Card <Card
ref={modalRef} ref={modalRef}
className="relative w-[500px] max-w-full bg-bgApp rounded-xl my-10 max-h-[90vh] flex flex-col" className="relative w-[500px] max-w-full bg-bgApp rounded-xl my-10 max-h-[90vh] flex flex-col shadow-xl z-[10000]"
> >
<div className="p-8 max-h-[calc(90vh-180px)] overflow-y-auto">{children}</div> <div className="p-8 max-h-[calc(90vh-180px)] overflow-y-auto">{children}</div>
{footer && ( {footer && (
@@ -6,6 +6,7 @@ import EnvVarsSection from './EnvVarsSection';
import ExtensionConfigFields from './ExtensionConfigFields'; import ExtensionConfigFields from './ExtensionConfigFields';
import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react'; import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
import ExtensionInfoFields from './ExtensionInfoFields'; import ExtensionInfoFields from './ExtensionInfoFields';
import ExtensionTimeoutField from './ExtensionTimeoutField';
interface ExtensionModalProps { interface ExtensionModalProps {
title: string; 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 // Form validation
const isFormValid = () => { const isFormValid = () => {
return isNameValid() && isConfigValid() && isEnvVarsValid(); return isNameValid() && isConfigValid() && isEnvVarsValid() && isTimeoutValid();
}; };
// Handle submit with validation // Handle submit with validation
@@ -94,9 +109,20 @@ export default function ExtensionModal({
setSubmitAttempted(true); setSubmitAttempted(true);
if (isFormValid()) { 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 // Create footer buttons based on current state
@@ -186,7 +212,7 @@ export default function ExtensionModal({
/> />
{/* Divider */} {/* Divider */}
<hr className="border-t border-borderSubtle mb-6" /> <hr className="border-t border-borderSubtle mb-4" />
{/* Command */} {/* Command */}
<div className="mb-6"> <div className="mb-6">
@@ -198,10 +224,16 @@ export default function ExtensionModal({
submitAttempted={submitAttempted} submitAttempted={submitAttempted}
isValid={isConfigValid()} isValid={isConfigValid()}
/> />
<div className="mb-4" />
<ExtensionTimeoutField
timeout={formData.timeout}
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
submitAttempted={submitAttempted}
/>
</div> </div>
{/* Divider */} {/* Divider */}
<hr className="border-t border-borderSubtle mb-6" /> <hr className="border-t border-borderSubtle mb-4" />
{/* Environment Variables */} {/* Environment Variables */}
<div className="mb-6"> <div className="mb-6">
@@ -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 (
<div className="flex flex-col gap-4 mb-6">
{/* Row with Timeout and timeout input side by side */}
<div className="flex justify-between gap-4">
<div className="flex-1">
<label className="text-sm font-medium mb-2 block text-textStandard">Timeout</label>
</div>
{/* Type Dropdown */}
<div className="w-[200px]">
<div className="relative">
<Input
value={timeout}
onChange={(e) => onChange('timeout', e.target.value)}
defaultValue={300}
className={`${!submitAttempted || isTimeoutValid() ? 'border-borderSubtle' : 'border-red-500'} text-textStandard focus:border-borderStandard`}
/>
{submitAttempted && !isTimeoutValid() && (
<div className="absolute text-xs text-red-500 mt-1">Timeout </div>
)}
</div>
</div>
</div>
</div>
);
}
@@ -1,5 +1,7 @@
// Default extension timeout in seconds // Default extension timeout in seconds
// TODO: keep in sync with rust better // TODO: keep in sync with rust better
import * as module from 'node:module';
export const DEFAULT_EXTENSION_TIMEOUT = 300; export const DEFAULT_EXTENSION_TIMEOUT = 300;
/** /**
@@ -24,6 +26,7 @@ export interface ExtensionFormData {
cmd?: string; cmd?: string;
endpoint?: string; endpoint?: string;
enabled: boolean; enabled: boolean;
timeout?: number;
envVars: { key: string; value: string }[]; envVars: { key: string; value: string }[];
} }
@@ -35,6 +38,7 @@ export function getDefaultFormData(): ExtensionFormData {
cmd: '', cmd: '',
endpoint: '', endpoint: '',
enabled: true, enabled: true,
timeout: 300,
envVars: [], envVars: [],
}; };
} }
@@ -59,6 +63,7 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
endpoint: extension.type === 'sse' ? extension.uri : undefined, endpoint: extension.type === 'sse' ? extension.uri : undefined,
enabled: extension.enabled, enabled: extension.enabled,
timeout: extension.timeout,
envVars, envVars,
}; };
} }
@@ -84,6 +89,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
description: formData.description, description: formData.description,
cmd: cmd, cmd: cmd,
args: args, args: args,
timeout: formData.timeout,
...(Object.keys(envs).length > 0 ? { envs } : {}), ...(Object.keys(envs).length > 0 ? { envs } : {}),
}; };
} else if (formData.type === 'sse') { } else if (formData.type === 'sse') {
@@ -91,6 +97,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
type: 'sse', type: 'sse',
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
timeout: formData.timeout,
uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type
...(Object.keys(envs).length > 0 ? { envs } : {}), ...(Object.keys(envs).length > 0 ? { envs } : {}),
}; };
@@ -99,6 +106,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
return { return {
type: formData.type, type: formData.type,
name: formData.name, name: formData.name,
timeout: formData.timeout,
}; };
} }
} }