ui: fix onboarding & allow for key deletion (#1928)

This commit is contained in:
Lily Delalande
2025-03-31 13:00:07 -04:00
committed by GitHub
parent 28cdd0381e
commit 780194798e
11 changed files with 214 additions and 56 deletions
@@ -123,7 +123,14 @@ pub async fn remove_config(
let config = Config::global();
match config.delete(&query.key) {
// Check if the secret flag is true and call the appropriate method
let result = if query.is_secret {
config.delete_secret(&query.key)
} else {
config.delete(&query.key)
};
match result {
Ok(_) => Ok(Json(format!("Removed key {}", query.key))),
Err(_) => Err(StatusCode::NOT_FOUND),
}
@@ -37,5 +37,5 @@ export async function getProviderMetadata(
if (!matches) {
throw Error(`No match for provider: ${providerName}`);
}
return matches[0].metadata;
return matches.metadata;
}
@@ -42,6 +42,21 @@ const ProviderCards = memo(function ProviderCards({
[openModal, refreshProviders]
);
const deleteProviderConfigViaModal = useCallback(
(provider: ProviderDetails) => {
openModal(provider, {
onDelete: () => {
// Only refresh if the function is provided
if (refreshProviders) {
refreshProviders();
}
},
formProps: {},
});
},
[openModal, refreshProviders]
);
// We don't need an intermediate function here
// Just pass the onProviderLaunch directly
@@ -52,6 +67,7 @@ const ProviderCards = memo(function ProviderCards({
key={provider.name}
provider={provider}
onConfigure={() => configureProviderViaModal(provider)}
onDelete={() => deleteProviderConfigViaModal(provider)}
onLaunch={() => onProviderLaunch(provider)}
isOnboarding={isOnboarding}
/>
@@ -87,6 +103,5 @@ export default memo(function ProviderGrid({
),
[providers, isOnboarding, refreshProviders, onProviderLaunch]
);
return <GridLayout>{modalProviderContent}</GridLayout>;
});
@@ -79,15 +79,14 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
);
return (
<div className="h-screen w-full">
<div className="h-screen w-full flex flex-col">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
{isOnboarding && (
<div className="group/logo">
<WelcomeGooseLogo className="h-16 w-16 md:h-20 md:w-20 text-black dark:text-white" />
</div>
)}
<ScrollArea className="h-full w-full">
<ScrollArea className="flex-1 w-full">
{isOnboarding && (
<div className="group/logo flex justify-left pl-8">
<WelcomeGooseLogo className="h-16 w-16 md:h-20 md:w-20 text-black dark:text-white" />
</div>
)}
<div className="px-8 pt-6 pb-4">
{/* Only show back button if not in onboarding mode */}
{!isOnboarding && <BackButton onClick={onClose} />}
@@ -96,7 +95,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
</h1>
{isOnboarding && (
<p className="text-s text-textSubtle max-w-2xl pt-2">
Select an AI model provider to get started with goose. Youll need to use API keys
Select an AI model provider to get started with goose. You'll need to use API keys
generated by each provider which will be encrypted and stored locally. You can change
your provider at any time in settings.
</p>
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import Modal from '../../../../components/Modal';
import ProviderSetupHeader from './subcomponents/ProviderSetupHeader';
import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetupForm';
@@ -10,6 +10,7 @@ import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHand
import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler';
import OllamaForm from './subcomponents/forms/OllamaForm';
import { useConfig } from '../../../ConfigContext';
import { AlertTriangle } from 'lucide-react';
const customSubmitHandlerMap = {
provider_name: OllamaSubmitHandler, // example
@@ -20,15 +21,31 @@ const customFormsMap = {
};
export default function ProviderConfigurationModal() {
const { upsert } = useConfig();
const [validationErrors, setValidationErrors] = useState({});
const { upsert, remove } = useConfig();
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
const [configValues, setConfigValues] = useState({});
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
useEffect(() => {
if (isOpen && currentProvider) {
// Reset form state when the modal opens with a new provider
setConfigValues({});
setValidationErrors({});
setShowDeleteConfirmation(false);
}
}, [isOpen, currentProvider]);
if (!isOpen || !currentProvider) return null;
const headerText = `Configure ${currentProvider.metadata.display_name}`;
const descriptionText = `Add your API key(s) for this provider to integrate into Goose`;
const isConfigured = currentProvider.is_configured;
const headerText = showDeleteConfirmation
? `Delete configuration for ${currentProvider.metadata.display_name}`
: `Configure ${currentProvider.metadata.display_name}`;
const descriptionText = showDeleteConfirmation
? 'This will permanently delete the current provider configuration.'
: `Add your API key(s) for this provider to integrate into Goose`;
const SubmitHandler = customSubmitHandlerMap[currentProvider.name] || DefaultSubmitHandler;
const FormComponent = customFormsMap[currentProvider.name] || DefaultProviderSetupForm;
@@ -80,6 +97,9 @@ export default function ProviderConfigurationModal() {
};
const handleCancel = () => {
// Reset delete confirmation state
setShowDeleteConfirmation(false);
// Use custom cancel handler if provided
if (modalProps.onCancel) {
modalProps.onCancel();
@@ -88,30 +108,87 @@ export default function ProviderConfigurationModal() {
closeModal();
};
const handleDelete = () => {
setShowDeleteConfirmation(true);
};
const handleConfirmDelete = async () => {
try {
// Remove the provider configuration
// get the keys
const params = currentProvider.metadata.config_keys;
// go through the keys are remove them
for (const param of params) {
console.log('param', param.name, 'secret', param.secret);
await remove(param.name, param.secret);
}
// Call onDelete callback if provided
// This should trigger the refreshProviders function
if (modalProps.onDelete) {
modalProps.onDelete(currentProvider.name);
}
// Reset the delete confirmation state before closing
setShowDeleteConfirmation(false);
// Close the modal
// Close the modal after deletion and callback
closeModal();
} catch (error) {
console.error('Failed to delete provider:', error);
// Keep modal open if there's an error
}
};
// Function to determine which icon to display
const getModalIcon = () => {
if (showDeleteConfirmation) {
return <AlertTriangle className="text-red-500" size={24} />;
}
return <ProviderLogo providerName={currentProvider.name} />;
};
return (
<Modal
onClose={closeModal}
footer={<ProviderSetupActions onCancel={handleCancel} onSubmit={handleSubmitForm} />}
footer={
<ProviderSetupActions
onCancel={handleCancel}
onSubmit={handleSubmitForm}
onDelete={handleDelete}
showDeleteConfirmation={showDeleteConfirmation}
onConfirmDelete={handleConfirmDelete}
onCancelDelete={() => setShowDeleteConfirmation(false)}
canDelete={isConfigured}
providerName={currentProvider.metadata.display_name}
/>
}
>
<div className="space-y-1">
{/* Logo area - centered above title */}
<ProviderLogo providerName={currentProvider.name} />
{/* Logo area or warning icon */}
<div>{getModalIcon()}</div>
{/* Title and some information - centered */}
<ProviderSetupHeader title={headerText} body={descriptionText} />
</div>
{/* Contains information used to set up each provider */}
<FormComponent
configValues={configValues}
setConfigValues={setConfigValues}
provider={currentProvider}
validationErrors={validationErrors}
{...(modalProps.formProps || {})} // Spread any custom form props
/>
{/* Only show the form when NOT in delete confirmation mode */}
{!showDeleteConfirmation ? (
<>
{/* Contains information used to set up each provider */}
<FormComponent
configValues={configValues}
setConfigValues={setConfigValues}
provider={currentProvider}
validationErrors={validationErrors}
{...(modalProps.formProps || {})} // Spread any custom form props
/>
{currentProvider.metadata.config_keys && currentProvider.metadata.config_keys.length > 0 && (
<SecureStorageNotice />
)}
{currentProvider.metadata.config_keys &&
currentProvider.metadata.config_keys.length > 0 && <SecureStorageNotice />}
</>
) : null}
</Modal>
);
}
@@ -4,6 +4,7 @@ import { ProviderDetails } from '../../../../api/types.gen';
interface ModalProps {
onSubmit?: (values: any) => void;
onCancel?: () => void;
onDelete?: (values: any) => void;
formProps?: any;
}
@@ -1,9 +0,0 @@
// used both for initial config and editing config
import ProviderDetails from '../../interfaces/ProviderDetails';
export default interface ProviderConfiguationModalProps {
provider: ProviderDetails;
title?: string;
onSubmit: (configValues: { [key: string]: string }) => void;
onCancel: () => void;
}
@@ -1,19 +1,71 @@
import React from 'react';
import { Button } from '../../../../ui/button';
import { Trash2 } from 'lucide-react';
interface ProviderSetupActionsProps {
onCancel: () => void;
onSubmit: (e: any) => void;
onDelete?: () => void;
showDeleteConfirmation?: boolean;
onConfirmDelete?: () => void;
onCancelDelete?: () => void;
canDelete?: boolean;
providerName?: string;
}
/**
* Renders the "Submit" and "Cancel" buttons at the bottom.
* Updated to match the design from screenshots.
* Renders the action buttons at the bottom of the provider modal.
* Includes submit, cancel, and delete functionality with confirmation.
*/
export default function ProviderSetupActions({ onCancel, onSubmit }: ProviderSetupActionsProps) {
export default function ProviderSetupActions({
onCancel,
onSubmit,
onDelete,
showDeleteConfirmation,
onConfirmDelete,
onCancelDelete,
canDelete,
providerName,
}: ProviderSetupActionsProps) {
// If we're showing delete confirmation, render the delete confirmation buttons
if (showDeleteConfirmation) {
return (
<>
<div className="w-full px-6 py-4 bg-red-900/20 border-t border-red-500/30">
<p className="text-red-400 text-sm mb-2">
Are you sure you want to delete the configuration parameters for {providerName}? This
action cannot be undone.
</p>
</div>
<Button
onClick={onConfirmDelete}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-red-900/20 text-red-500 font-medium text-md"
>
<Trash2 className="h-4 w-4 mr-2" /> Confirm Delete
</Button>
<Button
variant="ghost"
onClick={onCancelDelete}
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
>
Cancel
</Button>
</>
);
}
// Regular buttons (with delete if applicable)
return (
<div className="-ml-8 -mr-8">
{/* We rely on the <form> "onSubmit" for the actual Submit logic */}
{canDelete && onDelete && (
<Button
type="button"
onClick={onDelete}
className="w-full h-[60px] rounded-none border-t border-borderSubtle bg-transparent hover:bg-bgSubtle text-red-500 font-medium text-md"
>
<Trash2 className="h-4 w-4 mr-2" /> Delete Provider
</Button>
)}
<Button
type="submit"
variant="ghost"
@@ -13,12 +13,14 @@ export default function DefaultProviderSetupForm({
configValues,
setConfigValues,
provider,
validationErrors,
validationErrors = {},
}: DefaultProviderSetupFormProps) {
const parameters = provider.metadata.config_keys || [];
const [isLoading, setIsLoading] = useState(true);
const { read } = useConfig();
console.log('configValues default form', configValues);
// Initialize values when the component mounts or provider changes
useEffect(() => {
const loadConfigValues = async () => {
@@ -66,8 +68,8 @@ export default function DefaultProviderSetupForm({
setIsLoading(false);
};
loadConfigValues();
}, [provider.name, parameters, setConfigValues, read]);
loadConfigValues().then();
}, []);
// Filter parameters to only show required ones
const requiredParameters = useMemo(() => {
@@ -89,6 +91,7 @@ export default function DefaultProviderSetupForm({
return <div className="text-center py-4">Loading configuration values...</div>;
}
console.log('required params', requiredParameters);
return (
<div className="mt-4 space-y-4">
{requiredParameters.length === 0 ? (
@@ -109,8 +112,10 @@ export default function DefaultProviderSetupForm({
}))
}
placeholder={getPlaceholder(parameter)}
className={`w-full h-14 px-4 font-regular rounded-lg border shadow-none ${
validationErrors[parameter.name] ? 'border-red-500' : 'border-gray-300'
className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${
validationErrors[parameter.name]
? 'border-2 border-red-500'
: 'border border-gray-300'
} bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900`}
required={true}
/>
@@ -34,23 +34,33 @@ export default function CardContainer({
}: CardContainerProps) {
return (
<div
className={`relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle ${
!grayedOut ? 'hover:bg-transparent hover:duration-300' : ''
}`}
className={`relative h-full p-[2px] overflow-hidden rounded-[9px] group/card
${
grayedOut
? 'bg-borderSubtle hover:bg-gray-700'
: 'bg-borderSubtle hover:bg-transparent hover:duration-300'
}`}
onClick={!grayedOut ? onClick : undefined}
style={{
cursor: !grayedOut && onClick ? 'pointer' : 'default',
opacity: !grayedOut ? '1' : '0.5',
}}
>
{!grayedOut && <GlowingRing />}
<div
className={`relative bg-bgApp rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col justify-between ${
!grayedOut ? 'hover:border-borderStandard' : ''
}`}
className={`relative bg-bgApp rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col justify-between
${
grayedOut
? 'border border-borderSubtle'
: 'border border-borderSubtle hover:border-borderStandard'
}`}
>
<HeaderContainer>{header}</HeaderContainer>
{body}
{/* Apply opacity only to the header when grayed out */}
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
<HeaderContainer>{header}</HeaderContainer>
</div>
{/* Body always at full opacity */}
<div>{body}</div>
</div>
</div>
);
@@ -9,6 +9,7 @@ type ProviderCardProps = {
provider: ProviderDetails;
onConfigure: () => void;
onLaunch: () => void;
onDelete: () => void;
isOnboarding: boolean;
};