chore: update extensions section to work with new endpoints (#1696)
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
"license": {
|
"license": {
|
||||||
"name": "Apache-2.0"
|
"name": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"version": "1.0.13"
|
"version": "1.0.14"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/config": {
|
"/config": {
|
||||||
|
|||||||
@@ -4,65 +4,29 @@ import { Switch } from '../../ui/switch';
|
|||||||
import { Plus, X } from 'lucide-react';
|
import { Plus, X } from 'lucide-react';
|
||||||
import { Gear } from '../../icons/Gear';
|
import { Gear } from '../../icons/Gear';
|
||||||
import { GPSIcon } from '../../ui/icons';
|
import { GPSIcon } from '../../ui/icons';
|
||||||
import { useConfig } from '../../ConfigContext';
|
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import { Input } from '../../ui/input';
|
import { Input } from '../../ui/input';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
||||||
|
import { ExtensionConfig } from '../../../api/types.gen';
|
||||||
interface ExtensionConfig {
|
|
||||||
args?: string[];
|
|
||||||
cmd?: string;
|
|
||||||
enabled: boolean;
|
|
||||||
envs?: Record<string, string>;
|
|
||||||
name: string;
|
|
||||||
type: 'stdio' | 'sse' | 'builtin';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtensionItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
enabled: boolean;
|
|
||||||
canConfigure: boolean;
|
|
||||||
config: ExtensionConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnvVar {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get a friendly title from extension name
|
|
||||||
const getFriendlyTitle = (name: string): string => {
|
|
||||||
return name
|
|
||||||
.split('-')
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to get a subtitle based on extension type and configuration
|
|
||||||
const getSubtitle = (config: ExtensionConfig): string => {
|
|
||||||
if (config.type === 'builtin') {
|
|
||||||
return 'Built-in extension';
|
|
||||||
}
|
|
||||||
return `${config.type.toUpperCase()} extension${config.cmd ? ` (${config.cmd})` : ''}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ExtensionsSection() {
|
export default function ExtensionsSection() {
|
||||||
const { config, read, updateExtension, addExtension } = useConfig();
|
const { toggleExtension, getExtensions, addExtension } = useConfig();
|
||||||
const [extensions, setExtensions] = useState<ExtensionItem[]>([]);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [selectedExtension, setSelectedExtension] = useState<ExtensionItem | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
||||||
|
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState<{
|
const [formData, setFormData] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
type: 'stdio' | 'sse';
|
type: 'stdio' | 'sse' | 'builtin';
|
||||||
cmd?: string;
|
cmd?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
envVars: EnvVar[];
|
envVars: { key: string; value: string }[];
|
||||||
}>({
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
type: 'stdio',
|
type: 'stdio',
|
||||||
@@ -73,63 +37,82 @@ export default function ExtensionsSection() {
|
|||||||
envVars: [],
|
envVars: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Helper function to get a friendly title from extension name
|
||||||
const extensions = read('extensions', false);
|
const getFriendlyTitle = (name: string): string => {
|
||||||
if (extensions) {
|
return name
|
||||||
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => {
|
.split('-')
|
||||||
const extensionConfig = ext as ExtensionConfig;
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
return {
|
.join(' ');
|
||||||
id: name,
|
};
|
||||||
title: getFriendlyTitle(name),
|
|
||||||
subtitle: getSubtitle(extensionConfig),
|
// Helper function to get a subtitle based on extension type and configuration
|
||||||
enabled: extensionConfig.enabled,
|
const getSubtitle = (config: ExtensionConfig): string => {
|
||||||
canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs,
|
if (config.type === 'builtin') {
|
||||||
config: extensionConfig,
|
return 'Built-in extension';
|
||||||
};
|
|
||||||
});
|
|
||||||
setExtensions(extensionItems);
|
|
||||||
}
|
}
|
||||||
}, [read]);
|
if (config.type === 'stdio') {
|
||||||
|
return `STDIO extension${config.cmd ? ` (${config.cmd})` : ''}`;
|
||||||
|
}
|
||||||
|
if (config.type === 'sse') {
|
||||||
|
return `SSE extension${config.uri ? ` (${config.uri})` : ''}`;
|
||||||
|
}
|
||||||
|
return `Unknown type of extension`;
|
||||||
|
};
|
||||||
|
const fetchExtensions = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const extensionsList = await getExtensions(true); // Force refresh
|
||||||
|
// Sort extensions by name to maintain consistent order
|
||||||
|
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
setExtensions(sortedExtensions);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load extensions');
|
||||||
|
console.error('Error loading extensions:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchExtensions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedExtension) {
|
if (selectedExtension) {
|
||||||
const envVars = selectedExtension.config.envs
|
// Type guard: Check if 'envs' property exists for this variant
|
||||||
? Object.entries(selectedExtension.config.envs).map(([key, value]) => ({
|
const hasEnvs = selectedExtension.type === 'sse' || selectedExtension.type === 'stdio';
|
||||||
key,
|
|
||||||
value: value as string,
|
const envVars =
|
||||||
}))
|
hasEnvs && selectedExtension.envs
|
||||||
: [];
|
? Object.entries(selectedExtension.envs).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: selectedExtension.config.name,
|
name: selectedExtension.name,
|
||||||
type: selectedExtension.config.type as 'stdio' | 'sse',
|
type: selectedExtension.type,
|
||||||
cmd: selectedExtension.config.type === 'stdio' ? selectedExtension.config.cmd : undefined,
|
cmd: selectedExtension.type === 'stdio' ? selectedExtension.cmd : undefined,
|
||||||
args: selectedExtension.config.args || [],
|
args: selectedExtension.type === 'stdio' ? selectedExtension.args : [],
|
||||||
endpoint:
|
endpoint: selectedExtension.type === 'sse' ? selectedExtension.uri : undefined,
|
||||||
selectedExtension.config.type === 'sse' ? selectedExtension.config.cmd : undefined,
|
enabled: selectedExtension.enabled,
|
||||||
enabled: selectedExtension.config.enabled,
|
|
||||||
envVars,
|
envVars,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedExtension]);
|
}, [selectedExtension]);
|
||||||
|
|
||||||
const handleExtensionToggle = async (id: string) => {
|
const handleExtensionToggle = async (name: string) => {
|
||||||
const extension = extensions.find((ext) => ext.id === id);
|
try {
|
||||||
if (extension) {
|
await toggleExtension(name);
|
||||||
const updatedConfig = {
|
fetchExtensions(); // Refresh the list after toggling
|
||||||
...extension.config,
|
} catch (error) {
|
||||||
enabled: !extension.config.enabled,
|
console.error('Failed to toggle extension:', error);
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateExtension(id, updatedConfig);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update extension:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigureClick = (extension: ExtensionItem) => {
|
const handleConfigureClick = (extension: FixedExtensionEntry) => {
|
||||||
setSelectedExtension(extension);
|
setSelectedExtension(extension);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -145,24 +128,35 @@ export default function ExtensionsSection() {
|
|||||||
{} as Record<string, string>
|
{} as Record<string, string>
|
||||||
);
|
);
|
||||||
|
|
||||||
const extensionConfig = {
|
let extensionConfig: ExtensionConfig;
|
||||||
name: formData.name,
|
|
||||||
type: formData.type,
|
if (formData.type === 'stdio') {
|
||||||
enabled: formData.enabled,
|
extensionConfig = {
|
||||||
envs,
|
type: 'stdio',
|
||||||
...(formData.type === 'stdio'
|
name: formData.name,
|
||||||
? {
|
cmd: formData.cmd,
|
||||||
cmd: formData.cmd,
|
args: formData.args,
|
||||||
args: formData.args,
|
...(Object.keys(envs).length > 0 ? { envs } : {}),
|
||||||
}
|
};
|
||||||
: {
|
} else if (formData.type === 'sse') {
|
||||||
cmd: formData.endpoint,
|
extensionConfig = {
|
||||||
}),
|
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
|
||||||
|
extensionConfig = {
|
||||||
|
type: formData.type,
|
||||||
|
name: formData.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addExtension(formData.name, extensionConfig);
|
await addExtension(formData.name, extensionConfig, formData.enabled);
|
||||||
handleModalClose();
|
handleModalClose();
|
||||||
|
fetchExtensions(); // Refresh the list after adding
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add extension:', error);
|
console.error('Failed to add extension:', error);
|
||||||
}
|
}
|
||||||
@@ -207,7 +201,6 @@ export default function ExtensionsSection() {
|
|||||||
envVars: newEnvVars,
|
envVars: newEnvVars,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveConfig = async () => {
|
const handleSaveConfig = async () => {
|
||||||
if (!selectedExtension) return;
|
if (!selectedExtension) return;
|
||||||
|
|
||||||
@@ -221,24 +214,36 @@ export default function ExtensionsSection() {
|
|||||||
{} as Record<string, string>
|
{} as Record<string, string>
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedConfig = {
|
let extensionConfig: ExtensionConfig;
|
||||||
name: formData.name,
|
|
||||||
type: formData.type,
|
if (formData.type === 'stdio') {
|
||||||
enabled: formData.enabled,
|
extensionConfig = {
|
||||||
envs,
|
type: 'stdio',
|
||||||
...(formData.type === 'stdio'
|
name: formData.name,
|
||||||
? {
|
cmd: formData.cmd,
|
||||||
cmd: formData.cmd,
|
args: formData.args,
|
||||||
args: formData.args,
|
...(Object.keys(envs).length > 0 ? { envs } : {}),
|
||||||
}
|
};
|
||||||
: {
|
} else if (formData.type === 'sse') {
|
||||||
cmd: formData.endpoint,
|
extensionConfig = {
|
||||||
}),
|
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
|
||||||
|
extensionConfig = {
|
||||||
|
type: formData.type,
|
||||||
|
name: formData.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateExtension(selectedExtension.id, updatedConfig);
|
// CHANGE: Use addExtension instead of updateExtension
|
||||||
|
await addExtension(formData.name, extensionConfig, formData.enabled);
|
||||||
handleModalClose();
|
handleModalClose();
|
||||||
|
fetchExtensions(); // Refresh the list after updating
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update extension configuration:', error);
|
console.error('Failed to update extension configuration:', error);
|
||||||
}
|
}
|
||||||
@@ -256,14 +261,17 @@ export default function ExtensionsSection() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{extensions.map((extension, index) => (
|
{extensions.map((extension, index) => (
|
||||||
<React.Fragment key={extension.id}>
|
<React.Fragment key={extension.name}>
|
||||||
<div className="flex items-center justify-between py-3">
|
<div className="flex items-center justify-between py-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="font-medium text-textStandard">{extension.title}</h3>
|
<h3 className="font-medium text-textStandard">
|
||||||
<p className="text-sm text-textSubtle">{extension.subtitle}</p>
|
{getFriendlyTitle(extension.name)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-textSubtle">{getSubtitle(extension)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{extension.canConfigure && (
|
{/* Only show config button for non-builtin extensions */}
|
||||||
|
{extension.type !== 'builtin' && (
|
||||||
<button
|
<button
|
||||||
className="text-textSubtle hover:text-textStandard"
|
className="text-textSubtle hover:text-textStandard"
|
||||||
onClick={() => handleConfigureClick(extension)}
|
onClick={() => handleConfigureClick(extension)}
|
||||||
@@ -273,8 +281,8 @@ export default function ExtensionsSection() {
|
|||||||
)}
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
checked={extension.enabled}
|
checked={extension.enabled}
|
||||||
onCheckedChange={() => handleExtensionToggle(extension.id)}
|
onCheckedChange={() => handleExtensionToggle(extension.name)}
|
||||||
className="bg-[#393838] [&_span[data-state]]:bg-white"
|
variant="mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,10 +337,10 @@ export default function ExtensionsSection() {
|
|||||||
<label className="text-sm font-medium mb-2 block">Type</label>
|
<label className="text-sm font-medium mb-2 block">Type</label>
|
||||||
<Select
|
<Select
|
||||||
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
||||||
onChange={(option) =>
|
onChange={(option: { value: string; label: string } | null) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
type: (option?.value as 'stdio' | 'sse') || 'stdio',
|
type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
options={[
|
options={[
|
||||||
@@ -464,7 +472,7 @@ export default function ExtensionsSection() {
|
|||||||
<label className="text-sm font-medium mb-2 block">Type</label>
|
<label className="text-sm font-medium mb-2 block">Type</label>
|
||||||
<Select
|
<Select
|
||||||
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
||||||
onChange={(option) =>
|
onChange={(option: { value: string; label: string } | null) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
type: (option?.value as 'stdio' | 'sse') || 'stdio',
|
type: (option?.value as 'stdio' | 'sse') || 'stdio',
|
||||||
|
|||||||
Reference in New Issue
Block a user