Skip to content
56 changes: 51 additions & 5 deletions ui/desktop/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,64 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { Card } from './ui/card';

interface ModalProps {
children: React.ReactNode;
footer?: React.ReactNode; // Optional footer
onClose: () => void; // Function to call when modal should close
preventBackdropClose?: boolean; // Optional prop to prevent closing on backdrop click
}

/**
* A reusable modal component that renders content with a semi-transparent backdrop and blur effect.
* Closes when clicking outside the modal or pressing Esc key.
*/
export default function Modal({ children }: ModalProps) {
export default function Modal({
children,
footer,
onClose,
preventBackdropClose = false,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);

// Handle click outside the modal content
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (preventBackdropClose) return;
// Check if the click was on the backdrop and not on the modal content
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose();
}
};

// Handle Esc key press
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};

// Add event listener
document.addEventListener('keydown', handleEscKey);

// Clean up
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [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]">
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-6">
<div className="space-y-6">{children}</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"
onClick={handleBackdropClick}
>
<Card
ref={modalRef}
className="relative w-[500px] max-w-full bg-bgApp rounded-xl shadow-none my-10 overflow-hidden max-h-[90vh] flex flex-col"
>
<div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">{children}</div>
{footer && (
<div className="border-t border-borderSubtle bg-bgApp w-full mt-auto">{footer}</div>
)}
</Card>
</div>
);
Expand Down
104 changes: 20 additions & 84 deletions ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { Button } from '../../ui/button';
import { Plus } from 'lucide-react';
import { GPSIcon } from '../../ui/icons';
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
import { ExtensionConfig } from '../../../api/types.gen';
import ExtensionList from './subcomponents/ExtensionList';
import ExtensionModal from './modal/ExtensionModal';
import {
createExtensionConfig,
ExtensionFormData,
extensionToFormData,
getDefaultFormData,
} from './utils';

export default function ExtensionsSection() {
const { toggleExtension, getExtensions, addExtension } = useConfig();
const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
Expand Down Expand Up @@ -74,6 +79,16 @@ export default function ExtensionsSection() {
}
};

const handleDeleteExtension = async (name: string) => {
try {
await removeExtension(name);
handleModalClose();
fetchExtensions(); // Refresh the list after deleting
} catch (error) {
console.error('Failed to delete extension:', error);
}
};

const handleModalClose = () => {
setIsModalOpen(false);
setIsAddModalOpen(false);
Expand Down Expand Up @@ -122,7 +137,9 @@ export default function ExtensionsSection() {
initialData={extensionToFormData(selectedExtension)}
onClose={handleModalClose}
onSubmit={handleUpdateExtension}
onDelete={handleDeleteExtension}
submitLabel="Save Changes"
modalType={'edit'}
/>
)}

Expand All @@ -134,90 +151,9 @@ export default function ExtensionsSection() {
onClose={handleModalClose}
onSubmit={handleAddExtension}
submitLabel="Add Extension"
modalType={'add'}
/>
)}
</section>
);
}

// Helper functions

export interface ExtensionFormData {
name: string;
type: 'stdio' | 'sse' | 'builtin';
cmd?: string;
args?: string[];
endpoint?: string;
enabled: boolean;
envVars: { key: string; value: string }[];
}

function getDefaultFormData(): ExtensionFormData {
return {
name: '',
type: 'stdio',
cmd: '',
args: [],
endpoint: '',
enabled: true,
envVars: [],
};
}

function extensionToFormData(extension: FixedExtensionEntry): ExtensionFormData {
// Type guard: Check if 'envs' property exists for this variant
const hasEnvs = extension.type === 'sse' || extension.type === 'stdio';

const envVars =
hasEnvs && extension.envs
? Object.entries(extension.envs).map(([key, value]) => ({
key,
value: value as string,
}))
: [];

return {
name: extension.name,
type: extension.type,
cmd: extension.type === 'stdio' ? extension.cmd : undefined,
args: extension.type === 'stdio' ? extension.args : [],
endpoint: extension.type === 'sse' ? extension.uri : undefined,
enabled: extension.enabled,
envVars,
};
}

function createExtensionConfig(formData: ExtensionFormData): ExtensionConfig {
const envs = formData.envVars.reduce(
(acc, { key, value }) => {
if (key) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);

if (formData.type === 'stdio') {
return {
type: 'stdio',
name: formData.name,
cmd: formData.cmd,
args: formData.args,
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else if (formData.type === 'sse') {
return {
type: 'sse',
name: formData.name,
uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else {
// For other types
return {
type: formData.type,
name: formData.name,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,89 @@
import React from 'react';
import { Button } from '../../../ui/button';
import { X } from 'lucide-react';
import { Plus, X } from 'lucide-react';
import { Input } from '../../../ui/input';

interface EnvVarsSectionProps {
envVars: { key: string; value: string }[];
onAdd: () => void;
onRemove: (index: number) => void;
onChange: (index: number, field: 'key' | 'value', value: string) => void;
submitAttempted: boolean;
isValid: boolean;
}

export default function EnvVarsSection({
envVars,
onAdd,
onRemove,
onChange,
submitAttempted,
isValid,
}: EnvVarsSectionProps) {
return (
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium">Environment Variables</label>
<Button onClick={onAdd} variant="ghost" className="text-sm hover:bg-subtle">
Add Variable
</Button>
<div className="relative mb-2">
{' '}
{/* Added relative positioning with minimal margin */}
<label className="text-sm font-medium text-textStandard mb-2 block">
Environment Variables
</label>
{submitAttempted && !isValid && (
<div className="text-xs text-red-500 mt-1">
{' '}
{/* Removed absolute positioning */}
Environment variables must consist of sets of variable names and values
</div>
)}
</div>

<div className="space-y-2">
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
{/* Existing environment variables */}
{envVars.map((envVar, index) => (
<div key={index} className="flex gap-2 items-start">
<Input
value={envVar.key}
onChange={(e) => onChange(index, 'key', e.target.value)}
placeholder="Key"
className="flex-1"
/>
<Input
value={envVar.value}
onChange={(e) => onChange(index, 'value', e.target.value)}
placeholder="Value"
className="flex-1"
/>
<React.Fragment key={index}>
<div className="relative">
<Input
value={envVar.key}
onChange={(e) => onChange(index, 'key', e.target.value)}
placeholder="Variable name"
className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`}
/>
</div>
<div className="relative">
<Input
value={envVar.value}
onChange={(e) => onChange(index, 'value', e.target.value)}
placeholder="Value"
className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`}
/>
</div>
<Button
onClick={() => onRemove(index)}
variant="ghost"
className="p-2 h-auto hover:bg-subtle"
className="group p-2 h-auto text-iconSubtle hover:bg-transparent min-w-[60px] flex justify-start"
>
<X className="h-4 w-4" />
<X className="h-3 w-3 text-gray-400 group-hover:text-white group-hover:drop-shadow-sm transition-all" />
</Button>
</div>
</React.Fragment>
))}

{/* Empty row with Add button */}
<Input
placeholder="Variable name"
className="w-full border-borderStandard text-textStandard"
disabled
/>
<Input
placeholder="Value"
className="w-full border-borderStandard text-textStandard"
disabled
/>
<Button
onClick={onAdd}
variant="ghost"
className="flex items-center justify-start gap-1 px-2 pr-4 text-s font-medium rounded-full dark:bg-slate-400 dark:text-gray-300 bg-gray-300 text-slate-400 dark:hover:bg-slate-300 hover:bg-gray-500 hover:text-white dark:hover:text-gray-900 transition-colors min-w-[60px] h-9"
>
<Plus className="h-3 w-3" /> Add
</Button>
</div>
</div>
);
Expand Down
Loading
Loading