Ensure adding/removing extensions refreshes extensions list (#3695)
This commit is contained in:
@@ -102,6 +102,23 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
[reloadConfig]
|
[reloadConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refreshExtensions = useCallback(async () => {
|
||||||
|
const result = await apiGetExtensions();
|
||||||
|
|
||||||
|
if (result.response.status === 422) {
|
||||||
|
throw new MalformedConfigError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error && !result.data) {
|
||||||
|
console.log(result.error);
|
||||||
|
return extensionsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionResponse: ExtensionResponse = result.data!;
|
||||||
|
setExtensionsList(extensionResponse.extensions);
|
||||||
|
return extensionResponse.extensions;
|
||||||
|
}, [extensionsList]);
|
||||||
|
|
||||||
const addExtension = useCallback(
|
const addExtension = useCallback(
|
||||||
async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
||||||
// remove shims if present
|
// remove shims if present
|
||||||
@@ -113,39 +130,30 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
|||||||
body: query,
|
body: query,
|
||||||
});
|
});
|
||||||
await reloadConfig();
|
await reloadConfig();
|
||||||
|
// Refresh extensions list after successful addition
|
||||||
|
await refreshExtensions();
|
||||||
},
|
},
|
||||||
[reloadConfig]
|
[reloadConfig, refreshExtensions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeExtension = useCallback(
|
const removeExtension = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
await apiRemoveExtension({ path: { name: name } });
|
await apiRemoveExtension({ path: { name: name } });
|
||||||
await reloadConfig();
|
await reloadConfig();
|
||||||
|
// Refresh extensions list after successful removal
|
||||||
|
await refreshExtensions();
|
||||||
},
|
},
|
||||||
[reloadConfig]
|
[reloadConfig, refreshExtensions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getExtensions = useCallback(
|
const getExtensions = useCallback(
|
||||||
async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
||||||
if (forceRefresh || extensionsList.length === 0) {
|
if (forceRefresh || extensionsList.length === 0) {
|
||||||
const result = await apiGetExtensions();
|
return await refreshExtensions();
|
||||||
|
|
||||||
if (result.response.status === 422) {
|
|
||||||
throw new MalformedConfigError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.error && !result.data) {
|
|
||||||
console.log(result.error);
|
|
||||||
return extensionsList;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensionResponse: ExtensionResponse = result.data!;
|
|
||||||
setExtensionsList(extensionResponse.extensions);
|
|
||||||
return extensionResponse.extensions;
|
|
||||||
}
|
}
|
||||||
return extensionsList;
|
return extensionsList;
|
||||||
},
|
},
|
||||||
[extensionsList]
|
[extensionsList, refreshExtensions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleExtension = useCallback(
|
const toggleExtension = useCallback(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
|||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { GPSIcon } from '../ui/icons';
|
import { GPSIcon } from '../ui/icons';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import ExtensionModal from '../settings/extensions/modal/ExtensionModal';
|
import ExtensionModal from '../settings/extensions/modal/ExtensionModal';
|
||||||
import {
|
import {
|
||||||
getDefaultFormData,
|
getDefaultFormData,
|
||||||
@@ -31,6 +31,13 @@ export default function ExtensionsView({
|
|||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const { addExtension } = useConfig();
|
const { addExtension } = useConfig();
|
||||||
|
|
||||||
|
// Trigger refresh when deep link config changes (i.e., when a deep link is processed)
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewOptions.deepLinkConfig) {
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
}
|
||||||
|
}, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]);
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
};
|
};
|
||||||
@@ -46,7 +53,7 @@ export default function ExtensionsView({
|
|||||||
setRefreshKey((prevKey) => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to activate extension:', error);
|
console.error('Failed to activate extension:', error);
|
||||||
// Even if activation fails, we don't reopen the modal
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ export default function ExtensionsSection({
|
|||||||
showEnvVars
|
showEnvVars
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update deep link state when props change
|
||||||
|
useEffect(() => {
|
||||||
|
setDeepLinkConfigStateVar(deepLinkConfig);
|
||||||
|
setShowEnvVarsStateVar(showEnvVars);
|
||||||
|
}, [deepLinkConfig, showEnvVars]);
|
||||||
|
|
||||||
|
// Reset deep link state when component is re-mounted (via key prop changes)
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Cleanup function to reset state when component unmounts
|
||||||
|
setDeepLinkConfigStateVar(null);
|
||||||
|
setShowEnvVarsStateVar(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchExtensions = useCallback(async () => {
|
const fetchExtensions = useCallback(async () => {
|
||||||
const extensionsList = await getExtensions(true); // Force refresh
|
const extensionsList = await getExtensions(true); // Force refresh
|
||||||
// Sort extensions by name to maintain consistent order
|
// Sort extensions by name to maintain consistent order
|
||||||
@@ -69,12 +84,6 @@ export default function ExtensionsSection({
|
|||||||
enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled,
|
enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(
|
|
||||||
'Setting extensions with selectedExtensions:',
|
|
||||||
selectedExtensions,
|
|
||||||
'Extensions:',
|
|
||||||
sortedExtensions
|
|
||||||
);
|
|
||||||
setExtensions(sortedExtensions);
|
setExtensions(sortedExtensions);
|
||||||
}, [getExtensions, disableConfiguration, selectedExtensions]);
|
}, [getExtensions, disableConfiguration, selectedExtensions]);
|
||||||
|
|
||||||
@@ -129,27 +138,33 @@ export default function ExtensionsSection({
|
|||||||
const extensionConfig = createExtensionConfig(formData);
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
try {
|
try {
|
||||||
await activateExtension({ addToConfig: addExtension, extensionConfig: extensionConfig });
|
await activateExtension({ addToConfig: addExtension, extensionConfig: extensionConfig });
|
||||||
|
// Immediately refresh the extensions list after successful activation
|
||||||
|
await fetchExtensions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to activate extension:', error);
|
console.error('Failed to activate extension:', error);
|
||||||
// Even if activation fails, we don't reopen the modal
|
await fetchExtensions();
|
||||||
} finally {
|
|
||||||
// Add a small delay to ensure backend has updated, then refresh the extensions list
|
|
||||||
setTimeout(async () => {
|
|
||||||
await fetchExtensions();
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateExtension = async (formData: ExtensionFormData) => {
|
const handleUpdateExtension = async (formData: ExtensionFormData) => {
|
||||||
|
if (!selectedExtension) {
|
||||||
|
console.error('No selected extension for update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Close the modal immediately
|
// Close the modal immediately
|
||||||
handleModalClose();
|
handleModalClose();
|
||||||
|
|
||||||
const extensionConfig = createExtensionConfig(formData);
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
|
const originalName = selectedExtension.name;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateExtension({
|
await updateExtension({
|
||||||
enabled: formData.enabled,
|
enabled: formData.enabled,
|
||||||
extensionConfig: extensionConfig,
|
extensionConfig: extensionConfig,
|
||||||
addToConfig: addExtension,
|
addToConfig: addExtension,
|
||||||
|
removeFromConfig: removeExtension,
|
||||||
|
originalName: originalName,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update extension:', error);
|
console.error('Failed to update extension:', error);
|
||||||
@@ -182,6 +197,11 @@ export default function ExtensionsSection({
|
|||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
setSelectedExtension(null);
|
setSelectedExtension(null);
|
||||||
|
|
||||||
|
// Clear any navigation state that might be cached
|
||||||
|
if (window.history.state?.deepLinkConfig) {
|
||||||
|
window.history.replaceState({}, '', window.location.hash);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -178,6 +178,6 @@ export async function removeFromAgent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeName(name: string) {
|
export function sanitizeName(name: string) {
|
||||||
return name.toLowerCase().replace(/-/g, '').replace(/_/g, '').replace(/\s/g, '');
|
return name.toLowerCase().replace(/-/g, '').replace(/_/g, '').replace(/\s/g, '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExtensionConfig } from '../../../api/types.gen';
|
import type { ExtensionConfig } from '../../../api/types.gen';
|
||||||
import { toastService, ToastServiceOptions } from '../../../toasts';
|
import { toastService, ToastServiceOptions } from '../../../toasts';
|
||||||
import { addToAgent, removeFromAgent } from './agent-api';
|
import { addToAgent, removeFromAgent, sanitizeName } from './agent-api';
|
||||||
|
|
||||||
interface ActivateExtensionProps {
|
interface ActivateExtensionProps {
|
||||||
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||||
@@ -129,46 +129,121 @@ export async function addToAgentOnStartup({
|
|||||||
interface UpdateExtensionProps {
|
interface UpdateExtensionProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||||
|
removeFromConfig: (name: string) => Promise<void>;
|
||||||
extensionConfig: ExtensionConfig;
|
extensionConfig: ExtensionConfig;
|
||||||
|
originalName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an extension configuration without changing its enabled state
|
* Updates an extension configuration, handling name changes
|
||||||
*/
|
*/
|
||||||
export async function updateExtension({
|
export async function updateExtension({
|
||||||
enabled,
|
enabled,
|
||||||
addToConfig,
|
addToConfig,
|
||||||
|
removeFromConfig,
|
||||||
extensionConfig,
|
extensionConfig,
|
||||||
|
originalName,
|
||||||
}: UpdateExtensionProps) {
|
}: UpdateExtensionProps) {
|
||||||
if (enabled) {
|
// Sanitize the new name to match the behavior when adding extensions
|
||||||
|
const sanitizedNewName = sanitizeName(extensionConfig.name);
|
||||||
|
const sanitizedOriginalName = originalName ? sanitizeName(originalName) : undefined;
|
||||||
|
|
||||||
|
// Check if the sanitized name has changed
|
||||||
|
const nameChanged = sanitizedOriginalName && sanitizedOriginalName !== sanitizedNewName;
|
||||||
|
|
||||||
|
if (nameChanged) {
|
||||||
|
// Handle name change: remove old extension and add new one
|
||||||
|
|
||||||
|
// First remove the old extension from agent (using original name)
|
||||||
try {
|
try {
|
||||||
// AddToAgent
|
await removeFromAgent(originalName!, { silent: true }); // Suppress removal toast since we'll show update toast
|
||||||
await addToAgent(extensionConfig);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[updateExtension]: Failed to add extension to agent during update:', error);
|
console.error('Failed to remove old extension from agent during rename:', error);
|
||||||
// Failed to add to agent -- show that error to user and do not update the config file
|
// Continue with the process even if agent removal fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old extension from config (using original name)
|
||||||
|
try {
|
||||||
|
await removeFromConfig(originalName!); // We know originalName is not undefined here because nameChanged is true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove old extension from config during rename:', error);
|
||||||
|
throw error; // This is more critical, so we throw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the extension config with the sanitized name
|
||||||
|
const sanitizedExtensionConfig = {
|
||||||
|
...extensionConfig,
|
||||||
|
name: sanitizedNewName,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new extension with sanitized name
|
||||||
|
if (enabled) {
|
||||||
|
try {
|
||||||
|
// AddToAgent with silent option to avoid duplicate toasts
|
||||||
|
await addToAgent(sanitizedExtensionConfig, { silent: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateExtension]: Failed to add renamed extension to agent:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to config with sanitized name
|
||||||
|
try {
|
||||||
|
await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateExtension]: Failed to add renamed extension to config:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then add to config
|
toastService.configure({ silent: false });
|
||||||
try {
|
|
||||||
await addToConfig(extensionConfig.name, extensionConfig, enabled);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[updateExtension]: Failed to update extension in config:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await addToConfig(extensionConfig.name, extensionConfig, enabled);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[updateExtension]: Failed to update disabled extension in config:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
// show a toast that it was successfully updated
|
|
||||||
toastService.success({
|
toastService.success({
|
||||||
title: `Update extension`,
|
title: `Update extension`,
|
||||||
msg: `Successfully updated ${extensionConfig.name} extension`,
|
msg: `Successfully updated ${sanitizedNewName} extension`,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Create a copy of the extension config with the sanitized name
|
||||||
|
const sanitizedExtensionConfig = {
|
||||||
|
...extensionConfig,
|
||||||
|
name: sanitizedNewName,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
try {
|
||||||
|
// AddToAgent with silent option to avoid duplicate toasts
|
||||||
|
await addToAgent(sanitizedExtensionConfig, { silent: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateExtension]: Failed to add extension to agent during update:', error);
|
||||||
|
// Failed to add to agent -- show that error to user and do not update the config file
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add to config
|
||||||
|
try {
|
||||||
|
await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateExtension]: Failed to update extension in config:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// show a toast that it was successfully updated
|
||||||
|
toastService.success({
|
||||||
|
title: `Update extension`,
|
||||||
|
msg: `Successfully updated ${sanitizedNewName} extension`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateExtension]: Failed to update disabled extension in config:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// show a toast that it was successfully updated
|
||||||
|
toastService.success({
|
||||||
|
title: `Update extension`,
|
||||||
|
msg: `Successfully updated ${sanitizedNewName} extension`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user