fix:dont allow builtin deletion (#797)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../../extensions';
|
} from '../../extensions';
|
||||||
import { ConfigureExtensionModal } from './extensions/ConfigureExtensionModal';
|
import { ConfigureExtensionModal } from './extensions/ConfigureExtensionModal';
|
||||||
import { ManualExtensionModal } from './extensions/ManualExtensionModal';
|
import { ManualExtensionModal } from './extensions/ManualExtensionModal';
|
||||||
|
import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExtensionModal';
|
||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import { RecentModelsRadio } from './models/RecentModels';
|
import { RecentModelsRadio } from './models/RecentModels';
|
||||||
import { ExtensionItem } from './extensions/ExtensionItem';
|
import { ExtensionItem } from './extensions/ExtensionItem';
|
||||||
@@ -167,6 +168,10 @@ export default function Settings() {
|
|||||||
navigate('/settings', { replace: true });
|
navigate('/settings', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBuiltIn = (extensionId: string) => {
|
||||||
|
return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
@@ -232,6 +237,7 @@ export default function Settings() {
|
|||||||
<ExtensionItem
|
<ExtensionItem
|
||||||
key={ext.id}
|
key={ext.id}
|
||||||
{...ext}
|
{...ext}
|
||||||
|
canConfigure={true} // Ensure gear icon always appears
|
||||||
onToggle={handleExtensionToggle}
|
onToggle={handleExtensionToggle}
|
||||||
onConfigure={(extension) => setExtensionBeingConfigured(extension)}
|
onConfigure={(extension) => setExtensionBeingConfigured(extension)}
|
||||||
/>
|
/>
|
||||||
@@ -244,17 +250,29 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<ConfigureExtensionModal
|
{extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id) ? (
|
||||||
isOpen={!!extensionBeingConfigured}
|
<ConfigureBuiltInExtensionModal
|
||||||
onClose={() => {
|
isOpen={!!extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id)}
|
||||||
setExtensionBeingConfigured(null);
|
onClose={() => {
|
||||||
// Clear URL parameters when closing manually
|
setExtensionBeingConfigured(null);
|
||||||
navigate('/settings', { replace: true });
|
navigate('/settings', { replace: true });
|
||||||
}}
|
}}
|
||||||
extension={extensionBeingConfigured}
|
extension={extensionBeingConfigured}
|
||||||
onSubmit={handleExtensionConfigSubmit}
|
onSubmit={handleExtensionConfigSubmit}
|
||||||
onRemove={handleExtensionRemove}
|
/>
|
||||||
/>
|
) : (
|
||||||
|
<ConfigureExtensionModal
|
||||||
|
isOpen={!!extensionBeingConfigured}
|
||||||
|
onClose={() => {
|
||||||
|
setExtensionBeingConfigured(null);
|
||||||
|
// Clear URL parameters when closing manually
|
||||||
|
navigate('/settings', { replace: true });
|
||||||
|
}}
|
||||||
|
extension={extensionBeingConfigured}
|
||||||
|
onSubmit={handleExtensionConfigSubmit}
|
||||||
|
onRemove={handleExtensionRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ManualExtensionModal
|
<ManualExtensionModal
|
||||||
isOpen={isManualModalOpen}
|
isOpen={isManualModalOpen}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card } from '../../ui/card';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
import { FullExtensionConfig } from '../../../extensions';
|
||||||
|
import { getApiUrl, getSecretKey } from '../../../config';
|
||||||
|
import { addExtension } from '../../../extensions';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
interface ConfigureExtensionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
extension: FullExtensionConfig | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigureBuiltInExtensionModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
extension,
|
||||||
|
}: ConfigureExtensionModalProps) {
|
||||||
|
const [envValues, setEnvValues] = React.useState<Record<string, string>>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
|
|
||||||
|
// Reset form when dialog closes or extension changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isOpen || !extension) {
|
||||||
|
setEnvValues({});
|
||||||
|
}
|
||||||
|
}, [isOpen, extension]);
|
||||||
|
|
||||||
|
const handleExtensionConfigSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!extension) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
// First store all environment variables
|
||||||
|
if (extension.env_keys?.length > 0) {
|
||||||
|
for (const envKey of extension.env_keys) {
|
||||||
|
const value = envValues[envKey];
|
||||||
|
if (!value) continue;
|
||||||
|
|
||||||
|
const storeResponse = await fetch(getApiUrl('/secrets/store'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': getSecretKey(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: envKey,
|
||||||
|
value: value.trim(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!storeResponse.ok) {
|
||||||
|
throw new Error(`Failed to store environment variable: ${envKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await addExtension(extension);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add system configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`Successfully configured the ${extension.name} extension`);
|
||||||
|
onSubmit();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error configuring extension:', error);
|
||||||
|
toast.error('Failed to configure extension');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!extension || !isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm">
|
||||||
|
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-[16px] pt-[24px] pb-0">
|
||||||
|
<div className="px-8 pb-0 space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="text-2xl font-regular dark:text-white text-gray-900">
|
||||||
|
Configure {extension.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleExtensionConfigSubmit}>
|
||||||
|
<div className="mt-[24px]">
|
||||||
|
{extension.env_keys?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||||
|
Please provide the required environment variables for this extension:
|
||||||
|
</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{extension.env_keys?.map((envVarName) => (
|
||||||
|
<div key={envVarName}>
|
||||||
|
<label
|
||||||
|
htmlFor={envVarName}
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||||
|
>
|
||||||
|
{envVarName}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id={envVarName}
|
||||||
|
name={envVarName}
|
||||||
|
placeholder={envVarName}
|
||||||
|
value={envValues[envVarName] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEnvValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[envVarName]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder:text-gray-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
This extension doesn't require any environment variables.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="mt-[8px] ml-[-24px] mr-[-24px] pt-[16px]">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 dark:border-gray-600 text-lg font-regular"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Saving...' : 'Save Configuration'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full h-[60px] rounded-none border-t dark:border-gray-600 text-gray-400 hover:bg-gray-50 dark:border-gray-600 text-lg font-regular"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,10 +5,11 @@ import { Gear } from '../../icons';
|
|||||||
type ExtensionItemProps = FullExtensionConfig & {
|
type ExtensionItemProps = FullExtensionConfig & {
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
onConfigure: (extension: FullExtensionConfig) => void;
|
onConfigure: (extension: FullExtensionConfig) => void;
|
||||||
|
canConfigure?: boolean; // Added optional prop here
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExtensionItem: React.FC<ExtensionItemProps> = (props) => {
|
export const ExtensionItem: React.FC<ExtensionItemProps> = (props) => {
|
||||||
const { id, name, description, enabled, onToggle, onConfigure } = props;
|
const { id, name, description, enabled, onToggle, onConfigure, canConfigure } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg py-2 mb-2">
|
<div className="rounded-lg py-2 mb-2">
|
||||||
@@ -20,9 +21,11 @@ export const ExtensionItem: React.FC<ExtensionItemProps> = (props) => {
|
|||||||
<p className="text-xs text-textSubtle mt-[2px]">{description}</p>
|
<p className="text-xs text-textSubtle mt-[2px]">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button onClick={() => onConfigure(props)} className="">
|
{canConfigure && ( // Conditionally render the gear icon
|
||||||
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
|
<button onClick={() => onConfigure(props)} className="">
|
||||||
</button>
|
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => onToggle(id)}
|
onClick={() => onToggle(id)}
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full ${
|
className={`relative inline-flex h-6 w-11 items-center rounded-full ${
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const BUILT_IN_EXTENSIONS = [
|
|||||||
id: 'computercontroller',
|
id: 'computercontroller',
|
||||||
name: 'Computer Controller',
|
name: 'Computer Controller',
|
||||||
description:
|
description:
|
||||||
"General computer control tools that doesn't require you to be a developer or engineer.",
|
"General computer control tools that don't require you to be a developer or engineer.",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
type: 'builtin',
|
type: 'builtin',
|
||||||
env_keys: [],
|
env_keys: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user