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

View File

@@ -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 (
<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}
style={{ isolation: 'isolate' }} /* Creates a new stacking context */
>
<Card
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>
{footer && (

View File

@@ -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 */}
<hr className="border-t border-borderSubtle mb-6" />
<hr className="border-t border-borderSubtle mb-4" />
{/* Command */}
<div className="mb-6">
@@ -198,10 +224,16 @@ export default function ExtensionModal({
submitAttempted={submitAttempted}
isValid={isConfigValid()}
/>
<div className="mb-4" />
<ExtensionTimeoutField
timeout={formData.timeout}
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
submitAttempted={submitAttempted}
/>
</div>
{/* Divider */}
<hr className="border-t border-borderSubtle mb-6" />
<hr className="border-t border-borderSubtle mb-4" />
{/* Environment Variables */}
<div className="mb-6">

View File

@@ -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>
);
}

View File

@@ -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,
};
}
}