ui: auto update card upon config (#1610)

This commit is contained in:
Lily Delalande
2025-03-11 17:02:32 -07:00
committed by GitHub
parent abf44e2197
commit 4aafc49707
9 changed files with 173 additions and 154 deletions
@@ -11,9 +11,9 @@ use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::env;
use utoipa::ToSchema;
use crate::routes::utils::check_provider_configured;
use crate::state::AppState;
fn verify_secret_key(headers: &HeaderMap, state: &AppState) -> Result<StatusCode, StatusCode> {
@@ -123,7 +123,7 @@ pub async fn remove_config(
}
#[utoipa::path(
post, // Change from get to post
post,
path = "/config/read",
request_body = ConfigKeyQuery, // Switch back to request_body
responses(
@@ -335,31 +335,6 @@ pub async fn providers(
Ok(Json(providers_response))
}
fn check_provider_configured(metadata: &ProviderMetadata) -> bool {
let config = Config::global();
// Check all required keys for the provider
for key in &metadata.config_keys {
if key.required {
let key_name = &key.name;
// First, check if the key is set in the environment
let is_set_in_env = env::var(key_name).is_ok();
// If not set in environment, check the config file based on whether it's a secret or not
let is_set_in_config = config.get(key_name, key.secret).is_ok();
// If the key is neither in the environment nor in the config, the provider is not configured
if !is_set_in_env && !is_set_in_config {
return false;
}
}
}
// If all required keys are accounted for, the provider is considered configured
true
}
pub fn routes(state: AppState) -> Router {
Router::new()
.route("/config", get(read_all_config))
+1 -1
View File
@@ -6,7 +6,7 @@ pub mod extension;
pub mod health;
pub mod reply;
pub mod session;
pub mod utils;
use axum::Router;
// Function to configure all routes
+66 -18
View File
@@ -1,13 +1,15 @@
use serde::{Deserialize, Serialize};
use std::error::Error;
use goose::config::Config;
use goose::providers::base::{ConfigKey, ProviderMetadata};
use serde::{Deserialize, Serialize};
use std::env;
use std::error::Error;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum KeyLocation {
Environment,
ConfigFile,
Keychain,
NotFound
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -20,10 +22,8 @@ pub struct KeyInfo {
}
/// Inspects a configuration key to determine if it's set, its location, and value (for non-secret keys)
pub fn inspect_key(
key_name: &str,
is_secret: bool,
) -> Result<KeyInfo, Box<dyn Error>> {
#[allow(dead_code)]
pub fn inspect_key(key_name: &str, is_secret: bool) -> Result<KeyInfo, Box<dyn Error>> {
let config = Config::global();
// Check environment variable first
@@ -44,7 +44,7 @@ pub fn inspect_key(
let config_result = if is_secret {
config.get_secret(key_name).map(|v| (v, true))
} else {
config.get(key_name).map(|v| (v, false))
config.get_param(key_name).map(|v| (v, false))
};
match config_result {
@@ -64,20 +64,19 @@ pub fn inspect_key(
// Only include value for non-secret keys
value: if !is_secret_actual { Some(value) } else { None },
})
},
Err(_) => {
Ok(KeyInfo {
name: key_name.to_string(),
is_set: false,
location: KeyLocation::NotFound,
is_secret,
value: None,
})
}
Err(_) => Ok(KeyInfo {
name: key_name.to_string(),
is_set: false,
location: KeyLocation::NotFound,
is_secret,
value: None,
}),
}
}
/// Inspects multiple keys at once
#[allow(dead_code)]
pub fn inspect_keys(
keys: &[(String, bool)], // (name, is_secret) pairs
) -> Result<Vec<KeyInfo>, Box<dyn Error>> {
@@ -89,4 +88,53 @@ pub fn inspect_keys(
}
Ok(results)
}
}
pub fn check_provider_configured(metadata: &ProviderMetadata) -> bool {
let config = Config::global();
// Get all required keys
let required_keys: Vec<&ConfigKey> = metadata
.config_keys
.iter()
.filter(|key| key.required)
.collect();
// Special case: If a provider has exactly one required key and that key
// has a default value, check if it's explicitly set
if required_keys.len() == 1 && required_keys[0].default.is_some() {
let key = &required_keys[0];
// Check if the key is explicitly set (either in env or config)
let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();
return is_set_in_env || is_set_in_config;
}
// For providers with multiple keys or keys without defaults:
// Find required keys that don't have default values
let required_non_default_keys: Vec<&ConfigKey> = required_keys
.iter()
.filter(|key| key.default.is_none())
.cloned()
.collect();
// If there are no non-default keys, this provider needs at least one key explicitly set
if required_non_default_keys.is_empty() {
return required_keys.iter().any(|key| {
let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();
is_set_in_env || is_set_in_config
});
}
// Otherwise, all non-default keys must be set
required_non_default_keys.iter().all(|key| {
let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();
is_set_in_env || is_set_in_config
})
}
@@ -17,9 +17,11 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
const ProviderCards = memo(function ProviderCards({
providers,
isOnboarding,
refreshProviders,
}: {
providers: ProviderDetails[];
isOnboarding: boolean;
refreshProviders?: () => void;
}) {
const { openModal } = useProviderModal();
@@ -27,13 +29,16 @@ const ProviderCards = memo(function ProviderCards({
const configureProviderViaModal = useCallback(
(provider: ProviderDetails) => {
openModal(provider, {
onSubmit: (values: any) => {
// Your logic to save the configuration
onSubmit: () => {
// Only refresh if the function is provided
if (refreshProviders) {
refreshProviders();
}
},
formProps: {},
});
},
[openModal]
[openModal, refreshProviders]
);
const handleLaunch = useCallback(() => {
@@ -56,48 +61,28 @@ const ProviderCards = memo(function ProviderCards({
return <>{providerCards}</>;
});
// Fix the ProviderModalProvider
export const OptimizedProviderModalProvider = memo(function OptimizedProviderModalProvider({
children,
}: {
children: React.ReactNode;
}) {
const contextValue = useMemo(
() => ({
isOpen: false,
currentProvider: null,
modalProps: {},
openModal: (provider, additionalProps = {}) => {
// Implementation
},
closeModal: () => {
// Implementation
},
}),
[]
);
return <ProviderModalProvider>{children}</ProviderModalProvider>;
});
export default memo(function ProviderGrid({
providers,
isOnboarding,
refreshProviders,
}: {
providers: ProviderDetails[];
isOnboarding: boolean;
refreshProviders?: () => void;
}) {
// Remove the console.log
console.log('provider grid');
// Memoize the modal provider and its children to avoid recreating on every render
const modalProviderContent = useMemo(
() => (
<ProviderModalProvider>
<ProviderCards providers={providers} isOnboarding={isOnboarding} />
<ProviderCards
providers={providers}
isOnboarding={isOnboarding}
refreshProviders={refreshProviders}
/>
<ProviderConfigurationModal />
</ProviderModalProvider>
),
[providers, isOnboarding]
[providers, isOnboarding, refreshProviders]
);
return <GridLayout>{modalProviderContent}</GridLayout>;
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { ScrollArea } from '../../ui/scroll-area';
import BackButton from '../../ui/BackButton';
import ProviderGrid from './ProviderGrid';
@@ -9,37 +9,40 @@ export default function ProviderSettings({ onClose }: { onClose: () => void }) {
const { getProviders } = useConfig();
const [loading, setLoading] = useState(true);
const [providers, setProviders] = useState<ProviderDetails[]>([]);
const initialLoadDone = useRef(false);
// Create a function to load providers that can be called multiple times
const loadProviders = useCallback(async () => {
setLoading(true);
try {
// Only force refresh when explicitly requested, not on initial load
const result = await getProviders(!initialLoadDone.current);
if (result) {
setProviders(result);
initialLoadDone.current = true;
}
} catch (error) {
console.error('Failed to load providers:', error);
} finally {
setLoading(false);
}
}, [getProviders]);
// Load providers only once when component mounts
useEffect(() => {
let isMounted = true;
const loadProviders = async () => {
try {
// Force refresh to ensure we have the latest data
const result = await getProviders(true);
// Only update state if component is still mounted
if (isMounted && result) {
setProviders(result);
}
} catch (error) {
console.error('Failed to load providers:', error);
} finally {
if (isMounted) {
setLoading(false);
}
}
};
loadProviders();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Intentionally not including loadProviders in deps to prevent reloading
// Cleanup function to prevent state updates on unmounted component
return () => {
isMounted = false;
};
}, []); // Empty dependency array ensures this only runs once
// This function will be passed to ProviderGrid for manual refreshes after config changes
const refreshProviders = useCallback(() => {
if (initialLoadDone.current) {
getProviders(true).then((result) => {
if (result) setProviders(result);
});
}
}, [getProviders]);
console.log(providers);
return (
<div className="h-screen w-full">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
@@ -61,7 +64,11 @@ export default function ProviderSettings({ onClose }: { onClose: () => void }) {
{loading ? (
<div>Loading providers...</div>
) : (
<ProviderGrid providers={providers} isOnboarding={false} />
<ProviderGrid
providers={providers}
isOnboarding={false}
refreshProviders={refreshProviders}
/>
)}
</div>
</div>
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import Modal from '../../../../components/Modal';
import ProviderSetupHeader from './subcomponents/ProviderSetupHeader';
import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetupForm';
@@ -20,7 +20,7 @@ const customFormsMap = {
};
export default function ProviderConfigurationModal() {
const { upsert } = useConfig();
const { upsert, getProviders } = useConfig();
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
const [configValues, setConfigValues] = useState({});
@@ -32,15 +32,25 @@ export default function ProviderConfigurationModal() {
const SubmitHandler = customSubmitHandlerMap[currentProvider.name] || DefaultSubmitHandler;
const FormComponent = customFormsMap[currentProvider.name] || DefaultProviderSetupForm;
const handleSubmitForm = (e) => {
const handleSubmitForm = async (e) => {
e.preventDefault();
console.log('Form submitted for:', currentProvider.name);
SubmitHandler(upsert, currentProvider, configValues);
try {
// Wait for the submission to complete
await SubmitHandler(upsert, currentProvider, configValues);
// Close the modal unless the custom handler explicitly returns false
// This gives custom handlers the ability to keep the modal open if needed
closeModal();
// Close the modal before triggering refreshes to avoid UI issues
closeModal();
// Call onSubmit callback if provided (from modal props)
if (modalProps.onSubmit) {
modalProps.onSubmit(configValues);
}
} catch (error) {
console.error('Failed to save configuration:', error);
// Keep modal open if there's an error
}
};
const handleCancel = () => {
@@ -1,60 +1,56 @@
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';
import { ProviderDetails } from '../../../../api';
import React, { createContext, useContext, useState } from 'react';
import { ProviderDetails } from '../../../../api/types.gen';
interface ModalProps {
onSubmit?: (values: any) => void;
onCancel?: () => void;
formProps?: any;
}
interface ProviderModalContextType {
isOpen: boolean;
currentProvider: ProviderDetails | null;
modalProps: any;
openModal: (provider: ProviderDetails, additionalProps: any) => void;
modalProps: ModalProps;
openModal: (provider: ProviderDetails, additionalProps?: ModalProps) => void;
closeModal: () => void;
}
const defaultContext: ProviderModalContextType = {
isOpen: false,
currentProvider: null,
modalProps: {},
openModal: () => {},
closeModal: () => {},
};
const ProviderModalContext = createContext<ProviderModalContextType>(defaultContext);
export const useProviderModal = () => useContext<ProviderModalContextType>(ProviderModalContext);
const ProviderModalContext = createContext<ProviderModalContextType | undefined>(undefined);
export const ProviderModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [currentProvider, setCurrentProvider] = useState<ProviderDetails | null>(null);
const [modalProps, setModalProps] = useState({});
const [modalProps, setModalProps] = useState<ModalProps>({});
// Use useCallback to prevent function recreation on each render
const openModal = useCallback((provider: ProviderDetails, additionalProps = {}) => {
const openModal = (provider: ProviderDetails, additionalProps: ModalProps = {}) => {
setCurrentProvider(provider);
setModalProps(additionalProps);
setIsOpen(true);
}, []);
};
const closeModal = useCallback(() => {
const closeModal = () => {
setIsOpen(false);
// Use a small timeout to prevent UI flicker
setTimeout(() => {
setCurrentProvider(null);
setModalProps({});
}, 200);
}, []);
// Memoize the context value to prevent unnecessary re-renders
const contextValue = useMemo(
() => ({
isOpen,
currentProvider,
modalProps,
openModal,
closeModal,
}),
[isOpen, currentProvider, modalProps, openModal, closeModal]
);
};
return (
<ProviderModalContext.Provider value={contextValue}>{children}</ProviderModalContext.Provider>
<ProviderModalContext.Provider
value={{
isOpen,
currentProvider,
modalProps,
openModal,
closeModal,
}}
>
{children}
</ProviderModalContext.Provider>
);
};
export const useProviderModal = () => {
const context = useContext(ProviderModalContext);
if (context === undefined) {
throw new Error('useProviderModal must be used within a ProviderModalProvider');
}
return context;
};
@@ -25,12 +25,11 @@ export default function DefaultProviderSetupForm({
// Try to load actual values from config for each parameter that is not secret
for (const parameter of parameters) {
if (parameter.required && !parameter.secret) {
if (parameter.required) {
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
console.log('configResponse', configResponse);
if (configResponse) {
// Use the value from the config provider
@@ -101,8 +101,7 @@ export const DefaultSubmitHandler = async (upsertFn, provider, configValues) =>
// Create the provider-specific config key
const configKey = `${parameter.name}`;
// Explicitly define is_secret as a boolean (true/false) or null
// This is critical for Rust's Option<bool> type
// Explicitly define is_secret as a boolean (true/false)
const isSecret = parameter.secret === true;
// Pass the is_secret flag from the parameter definition