From 63b239a4cbcaa5f3dfd1de1b470c423e0bc01a95 Mon Sep 17 00:00:00 2001
From: lily-de <119957291+lily-de@users.noreply.github.com>
Date: Thu, 23 Jan 2025 11:19:27 -0800
Subject: [PATCH] feat: configure providers grid (#709)
---
.../settings/ProviderSetupModal.tsx | 6 +-
.../settings/providers/BaseProviderGrid.tsx | 226 +++++++++++++++++
.../settings/providers/ConfigureProviders.tsx | 11 +-
.../providers/ConfigureProvidersGrid.tsx | 231 ++++++++++++++++++
.../settings/providers/ProviderGrid.tsx | 44 ++--
.../providers/old_stuff/ProviderCard.tsx | 115 ---------
.../providers/old_stuff/ProvidersList.tsx | 16 --
.../settings/providers/old_stuff/types.ts | 38 ---
.../settings/providers/old_stuff/utils.ts | 68 ------
9 files changed, 489 insertions(+), 266 deletions(-)
create mode 100644 ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
create mode 100644 ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
delete mode 100644 ui/desktop/src/components/settings/providers/old_stuff/ProviderCard.tsx
delete mode 100644 ui/desktop/src/components/settings/providers/old_stuff/ProvidersList.tsx
delete mode 100644 ui/desktop/src/components/settings/providers/old_stuff/types.ts
delete mode 100644 ui/desktop/src/components/settings/providers/old_stuff/utils.ts
diff --git a/ui/desktop/src/components/settings/ProviderSetupModal.tsx b/ui/desktop/src/components/settings/ProviderSetupModal.tsx
index 9c1fc06d06..3d9b16aaff 100644
--- a/ui/desktop/src/components/settings/ProviderSetupModal.tsx
+++ b/ui/desktop/src/components/settings/ProviderSetupModal.tsx
@@ -3,12 +3,14 @@ import { Card } from '../ui/card';
import { Lock } from 'lucide-react';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
+import { required_keys } from './models/hardcoded_stuff';
// import UnionIcon from "../images/Union@2x.svg";
interface ProviderSetupModalProps {
provider: string;
model: string;
endpoint: string;
+ title?: string;
onSubmit: (apiKey: string) => void;
onCancel: () => void;
}
@@ -17,10 +19,12 @@ export function ProviderSetupModal({
provider,
model,
endpoint,
+ title,
onSubmit,
onCancel,
}: ProviderSetupModalProps) {
const [apiKey, setApiKey] = React.useState('');
+ const keyName = required_keys[provider]?.[0] || 'API Key';
const headerText = `Setup ${provider}`;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -48,7 +52,7 @@ export function ProviderSetupModal({
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
- placeholder={`Enter API key`}
+ placeholder={keyName}
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"
required
/>
diff --git a/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
new file mode 100644
index 0000000000..8236281520
--- /dev/null
+++ b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
@@ -0,0 +1,226 @@
+import React from 'react';
+import { Check, Plus, Settings, X } from 'lucide-react';
+import { Button } from '../../ui/button';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
+import { Portal } from '@radix-ui/react-portal';
+import { required_keys } from '../models/hardcoded_stuff';
+
+// Common interfaces and helper functions
+interface Provider {
+ id: string;
+ name: string;
+ isConfigured: boolean;
+ description: string;
+}
+
+interface BaseProviderCardProps {
+ name: string;
+ description: string;
+ isConfigured: boolean;
+ isSelected?: boolean;
+ isSelectable?: boolean;
+ onSelect?: () => void;
+ onAddKeys?: () => void;
+ onConfigure?: () => void;
+ showSettings?: boolean;
+ onDelete?: () => void;
+ showDelete?: boolean;
+ hasRequiredKeys?: boolean;
+}
+
+function getArticle(word: string): string {
+ return 'aeiouAEIOU'.indexOf(word[0]) >= 0 ? 'an' : 'a';
+}
+
+function BaseProviderCard({
+ name,
+ description,
+ isConfigured,
+ isSelected,
+ isSelectable,
+ onSelect,
+ onAddKeys,
+ onConfigure,
+ showSettings,
+ onDelete,
+ showDelete = false,
+ hasRequiredKeys = false,
+}: BaseProviderCardProps) {
+ const numRequiredKeys = required_keys[name]?.length || 0;
+ const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
+
+ return (
+
isSelectable && isConfigured && onSelect?.()}
+ className={`relative bg-white dark:bg-gray-800 rounded-lg border
+ ${
+ isSelected
+ ? 'border-blue-500 dark:border-blue-400 shadow-[0_0_0_1px] shadow-blue-500/50'
+ : 'border-gray-200 dark:border-gray-700'
+ }
+ p-3 transition-all duration-200 h-[140px]
+ ${isSelectable && isConfigured ? 'cursor-pointer' : ''}
+ ${!isSelectable ? 'hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md hover:scale-[1.01]' : ''}
+ ${isSelectable && isConfigured ? 'hover:border-blue-400 dark:hover:border-blue-300 hover:shadow-md' : ''}
+ `}
+ >
+ {isConfigured && (
+
+
+
+
+
+
+
+
+
+
+
+ {hasRequiredKeys
+ ? `You have ${getArticle(name)} ${name} API Key set in your environment`
+ : `${name} has no required API keys`}
+
+
+
+
+
+
+ {showDelete && hasRequiredKeys && (
+
+
+
+
+
+
+
+ Remove {name} API Key
+
+
+
+
+ )}
+
+ )}
+
+
+
+ {name}
+
+
+
+
+ {description}
+
+
+
+ {!isConfigured && onAddKeys && hasRequiredKeys && (
+
+
+
+
+
+
+
+ {tooltipText}
+
+
+
+
+ )}
+ {isConfigured && showSettings && hasRequiredKeys && (
+
+
+
+
+
+
+
+ Configure {name} settings
+
+
+
+
+ )}
+
+
+ );
+}
+
+interface BaseProviderGridProps {
+ providers: Provider[];
+ isSelectable?: boolean;
+ showSettings?: boolean;
+ showDelete?: boolean;
+ selectedId?: string | null;
+ onSelect?: (providerId: string) => void;
+ onAddKeys?: (provider: Provider) => void;
+ onConfigure?: (provider: Provider) => void;
+ onDelete?: (provider: Provider) => void;
+}
+
+export function BaseProviderGrid({
+ providers,
+ isSelectable = false,
+ showSettings = false,
+ showDelete = false,
+ selectedId = null,
+ onSelect,
+ onAddKeys,
+ onConfigure,
+ onDelete,
+}: BaseProviderGridProps) {
+ return (
+
+ {providers.map((provider) => {
+ const hasRequiredKeys = required_keys[provider.name]?.length > 0;
+ return (
+ onSelect?.(provider.id)}
+ onAddKeys={() => onAddKeys?.(provider)}
+ onConfigure={() => onConfigure?.(provider)}
+ onDelete={() => onDelete?.(provider)}
+ showSettings={showSettings}
+ showDelete={showDelete}
+ hasRequiredKeys={hasRequiredKeys}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/ui/desktop/src/components/settings/providers/ConfigureProviders.tsx b/ui/desktop/src/components/settings/providers/ConfigureProviders.tsx
index 9cc0e3481d..9d2b7faeed 100644
--- a/ui/desktop/src/components/settings/providers/ConfigureProviders.tsx
+++ b/ui/desktop/src/components/settings/providers/ConfigureProviders.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { Providers } from './Provider';
import { ScrollArea } from '../../ui/scroll-area';
import BackButton from '../../ui/BackButton';
+import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
export default function ConfigureProviders() {
return (
@@ -9,8 +10,8 @@ export default function ConfigureProviders() {
- {/* Left Navigation */}
-
+ {/* Left Navigation - further reduced width */}
+
@@ -18,11 +19,13 @@ export default function ConfigureProviders() {
{/* Content Area */}
-
+
Configure Providers
-
+
+
+
diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
new file mode 100644
index 0000000000..12a68807c4
--- /dev/null
+++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
@@ -0,0 +1,231 @@
+import React, { useMemo, useState } from 'react';
+import { useActiveKeys } from '../api_keys/ActiveKeysContext';
+import { BaseProviderGrid } from './BaseProviderGrid';
+import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff';
+import { getProviderDescription } from './Provider';
+import { ProviderSetupModal } from '../ProviderSetupModal';
+import { getApiUrl, getSecretKey } from '../../../config';
+import { toast } from 'react-toastify';
+import { getActiveProviders } from '../api_keys/utils';
+import { useModel } from '../models/ModelContext';
+import { Button } from '../../ui/button';
+
+function ConfirmationModal({ message, onConfirm, onCancel }) {
+ return (
+
+
+
+
+ Confirm Delete
+
+
{message}
+
+
+
+
+
+
+
+ );
+}
+
+// Settings version - non-selectable cards with settings gear
+export function ConfigureProvidersGrid() {
+ const { activeKeys, setActiveKeys } = useActiveKeys();
+ const [showSetupModal, setShowSetupModal] = useState(false);
+ const [selectedForSetup, setSelectedForSetup] = useState
(null);
+ const [modalMode, setModalMode] = useState<'edit' | 'setup'>('setup');
+ const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
+ const [providerToDelete, setProviderToDelete] = useState(null);
+ const { currentModel } = useModel();
+
+ const providers = useMemo(() => {
+ return supported_providers.map((providerName) => {
+ const alias =
+ provider_aliases.find((p) => p.provider === providerName)?.alias ||
+ providerName.toLowerCase();
+ const isConfigured = activeKeys.includes(providerName);
+
+ return {
+ id: alias,
+ name: providerName,
+ isConfigured,
+ description: getProviderDescription(providerName),
+ };
+ });
+ }, [activeKeys]);
+
+ const handleAddKeys = (provider) => {
+ setSelectedForSetup(provider.id);
+ setModalMode('setup');
+ setShowSetupModal(true);
+ };
+
+ const handleConfigure = (provider) => {
+ setSelectedForSetup(provider.id);
+ setModalMode('edit');
+ setShowSetupModal(true);
+ };
+
+ const handleModalSubmit = async (apiKey: string) => {
+ if (!selectedForSetup) return;
+
+ const provider = providers.find((p) => p.id === selectedForSetup)?.name;
+ if (!provider) return;
+
+ const keyName = required_keys[provider]?.[0];
+ if (!keyName) {
+ console.error(`No key found for provider ${provider}`);
+ return;
+ }
+
+ try {
+ // Always delete the old key first when editing
+ const deleteResponse = await fetch(getApiUrl('/secrets/delete'), {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Secret-Key': getSecretKey(),
+ },
+ body: JSON.stringify({ key: keyName }),
+ });
+
+ if (!deleteResponse.ok) {
+ const errorText = await deleteResponse.text();
+ console.error('Delete response error:', errorText);
+ throw new Error('Failed to delete old key');
+ }
+
+ // Store new key
+ const storeResponse = await fetch(getApiUrl('/secrets/store'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Secret-Key': getSecretKey(),
+ },
+ body: JSON.stringify({
+ key: keyName,
+ value: apiKey.trim(),
+ }),
+ });
+
+ if (!storeResponse.ok) {
+ const errorText = await storeResponse.text();
+ console.error('Store response error:', errorText);
+ throw new Error('Failed to store new key');
+ }
+
+ toast.success(`Successfully updated API key for ${provider}`);
+
+ const updatedKeys = await getActiveProviders();
+ setActiveKeys(updatedKeys);
+
+ setShowSetupModal(false);
+ setSelectedForSetup(null);
+ setModalMode('setup'); // Reset modal mode
+ } catch (error) {
+ console.error('Error handling modal submit:', error);
+ toast.error(`Failed to update API key for ${provider}`);
+ }
+ };
+
+ const handleDelete = async (provider) => {
+ setProviderToDelete(provider);
+ setIsConfirmationOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!providerToDelete) return;
+
+ const keyName = required_keys[providerToDelete.name]?.[0];
+ if (!keyName) {
+ console.error(`No key found for provider ${providerToDelete.name}`);
+ return;
+ }
+
+ try {
+ // Check if the selected provider is currently active
+ if (currentModel?.provider === providerToDelete.name) {
+ toast.error(
+ `Cannot delete the API key for ${providerToDelete.name} because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`
+ );
+ setIsConfirmationOpen(false);
+ return;
+ }
+
+ const deleteResponse = await fetch(getApiUrl('/secrets/delete'), {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Secret-Key': getSecretKey(),
+ },
+ body: JSON.stringify({ key: keyName }),
+ });
+
+ if (!deleteResponse.ok) {
+ const errorText = await deleteResponse.text();
+ console.error('Delete response error:', errorText);
+ throw new Error('Failed to delete key');
+ }
+
+ console.log('Key deleted successfully.');
+ toast.success(`Successfully deleted API key for ${providerToDelete.name}`);
+
+ const updatedKeys = await getActiveProviders();
+ setActiveKeys(updatedKeys);
+ } catch (error) {
+ console.error('Error deleting key:', error);
+ toast.error(`Unable to delete API key for ${providerToDelete.name}`);
+ }
+ setIsConfirmationOpen(false);
+ };
+
+ return (
+
+
+
+ {showSetupModal && selectedForSetup && (
+
+
p.id === selectedForSetup)?.name}
+ model="Example Model"
+ endpoint="Example Endpoint"
+ title={
+ modalMode === 'edit'
+ ? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} API Key`
+ : undefined
+ }
+ onSubmit={handleModalSubmit}
+ onCancel={() => {
+ setShowSetupModal(false);
+ setSelectedForSetup(null);
+ }}
+ />
+
+ )}
+
+ {isConfirmationOpen && providerToDelete && (
+
setIsConfirmationOpen(false)}
+ />
+ )}
+
+ );
+}
diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
index 974e61ef2c..c985af7b11 100644
--- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
+++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
@@ -15,6 +15,7 @@ import { toast } from 'react-toastify';
import { getActiveProviders } from '../api_keys/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
import { useNavigate } from 'react-router-dom';
+import { BaseProviderGrid } from './BaseProviderGrid';
interface ProviderCardProps {
name: string;
@@ -262,32 +263,27 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
settings.
-
- {providers.map((provider) => (
-
handleSelect(provider.id)}
- onConfigure={() => handleConfigure(provider)}
- onAddKeys={() => handleAddKeys(provider)}
- />
- ))}
-
+
{showSetupModal && selectedId && (
-
p.id === selectedId)?.name}
- model="Example Model"
- endpoint="Example Endpoint"
- onSubmit={handleModalSubmit}
- onCancel={() => {
- setShowSetupModal(false);
- setSelectedId(null);
- }}
- />
+
+
p.id === selectedId)?.name}
+ model="Example Model"
+ endpoint="Example Endpoint"
+ onSubmit={handleModalSubmit}
+ onCancel={() => {
+ setShowSetupModal(false);
+ setSelectedId(null);
+ }}
+ />
+
)}
);
diff --git a/ui/desktop/src/components/settings/providers/old_stuff/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/old_stuff/ProviderCard.tsx
deleted file mode 100644
index bd6f86fcd0..0000000000
--- a/ui/desktop/src/components/settings/providers/old_stuff/ProviderCard.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { FaKey, FaExclamationCircle, FaPencilAlt, FaTrash, FaPlus } from 'react-icons/fa';
-import React from 'react';
-
-export const ProviderCard = ({
- provider,
- secrets,
- isExpanded,
- isSupported,
- isChangingProvider,
- toggleProvider,
- handleAddOrEditKey,
- handleDeleteKey,
- handleSelectProvider,
- getProviderStatus,
-}) => {
- const hasUnsetKeys = getProviderStatus(provider);
-
- return (
-
-
-
-
-
- {isSupported && isExpanded && (
-
- {provider.keys.map((key) => {
- const secret = secrets.find((s) => s.key === key);
- return (
-
-
-
{key}
-
Source: {secret?.location || 'none'}
-
-
-
- {secret?.is_set ? 'Key set' : 'Missing'}
-
-
-
-
-
- );
- })}
-
- {provider.id.toLowerCase() !== localStorage.getItem('GOOSE_PROVIDER')?.toLowerCase() && (
-
- )}
-
- )}
-
- );
-};
diff --git a/ui/desktop/src/components/settings/providers/old_stuff/ProvidersList.tsx b/ui/desktop/src/components/settings/providers/old_stuff/ProvidersList.tsx
deleted file mode 100644
index 67eee69d73..0000000000
--- a/ui/desktop/src/components/settings/providers/old_stuff/ProvidersList.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-
-export const ProviderList = ({ providers, onProviderSelect }) => (
-
- {providers.map((provider) => (
-
- ))}
-
-);
diff --git a/ui/desktop/src/components/settings/providers/old_stuff/types.ts b/ui/desktop/src/components/settings/providers/old_stuff/types.ts
deleted file mode 100644
index a6e190c2dc..0000000000
--- a/ui/desktop/src/components/settings/providers/old_stuff/types.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// transformation of the response provided by secrets/provider endpoint
-export interface Provider {
- id: string;
- name: string;
- keys: string[];
- description: string;
- canDelete?: boolean;
- supported: boolean;
- order: number;
-}
-
-export interface SecretDetails {
- key: string;
- is_set: boolean;
- location?: string;
-}
-
-// returned by the secrets/providers endpoint
-export interface ProviderResponse {
- supported: boolean;
- name?: string;
- description?: string;
- models?: string[];
- secret_status: Record
;
-}
-
-// Represents the backend's secret structure for a single secret
-export interface RawSecretStatus {
- location: string; // Where the secret is stored (e.g., "keyring")
- is_set: boolean; // Whether the secret is configured
-}
-
-// Represents the transformed structure of a secret in the frontend
-export interface TransformedSecret {
- key: string; // The secret's key (e.g., "OPENAI_API_KEY")
- location: string; // Where the secret is stored (e.g., "keyring")
- is_set: boolean; // Whether the secret is set
-}
diff --git a/ui/desktop/src/components/settings/providers/old_stuff/utils.ts b/ui/desktop/src/components/settings/providers/old_stuff/utils.ts
deleted file mode 100644
index 237f3e88a4..0000000000
--- a/ui/desktop/src/components/settings/providers/old_stuff/utils.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { ProviderResponse, Provider, TransformedSecret, RawSecretStatus } from './types';
-import { getProvidersList } from '../../../../utils/providerUtils';
-import { getApiUrl, getSecretKey } from '../../../../config';
-
-export async function getSecretsSettings(): Promise> {
- const providerList = await getProvidersList();
- // Extract the list of IDs
- const providerIds = providerList.map((provider) => provider.id);
-
- // Fetch secrets state (set/unset) using the provider IDs
- const response = await fetch(getApiUrl('/secrets/providers'), {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Secret-Key': getSecretKey(),
- },
- body: JSON.stringify({
- providers: providerIds,
- }),
- });
-
- if (!response.ok) {
- throw new Error('Failed to fetch secrets');
- }
-
- const data = (await response.json()) as Record;
- return data;
-}
-
-export function transformProviderSecretsResponse(
- data: Record
-): Provider[] {
- // Transform the response into a list of ProviderWithSecrets objects
- const providerOrder = ['openai', 'anthropic', 'databricks']; // maintains these three at top of resulting list
- const transformedProviders: Provider[] = Object.entries(data)
- .map(([id, status]: [string, any]) => ({
- id: id.toLowerCase(),
- name: status.name ? status.name : id,
- keys: status.secret_status ? Object.keys(status.secret_status) : [],
- description: status.description ? status.description : 'Unsupported provider',
- supported: status.supported,
- canDelete: id.toLowerCase() !== 'openai' && id.toLowerCase() !== 'anthropic',
- order: providerOrder.indexOf(id.toLowerCase()),
- }))
- .sort((a, b) => {
- if (a.order !== -1 && b.order !== -1) {
- return a.order - b.order;
- }
- if (a.order === -1 && b.order === -1) {
- return a.name.localeCompare(b.name);
- }
- return a.order === -1 ? 1 : -1;
- });
-
- return transformedProviders;
-}
-
-export function transformSecrets(data: Record): TransformedSecret[] {
- return Object.entries(data)
- .filter(([_, provider]) => provider.supported && provider.secret_status)
- .flatMap(([_, provider]) =>
- Object.entries(provider.secret_status!).map(([key, rawStatus]) => ({
- key, // Secret key (e.g., "OPENAI_API_KEY")
- location: rawStatus.location || 'none', // Default location if missing
- is_set: rawStatus.is_set, // Renamed from `is_set` to `isSet`
- }))
- );
-}