mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 23:24:23 +01:00
feat: V1.0 (#734)
Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Wendy Tang <wendytang@squareup.com> Co-authored-by: Jarrod Sibbison <72240382+jsibbison-square@users.noreply.github.com> Co-authored-by: Alex Hancock <alex.hancock@example.com> Co-authored-by: Alex Hancock <alexhancock@block.xyz> Co-authored-by: Lifei Zhou <lifei@squareup.com> Co-authored-by: Wes <141185334+wesrblock@users.noreply.github.com> Co-authored-by: Max Novich <maksymstepanenko1990@gmail.com> Co-authored-by: Zaki Ali <zaki@squareup.com> Co-authored-by: Salman Mohammed <smohammed@squareup.com> Co-authored-by: Kalvin C <kalvinnchau@users.noreply.github.com> Co-authored-by: Alec Thomas <alec@swapoff.org> Co-authored-by: lily-de <119957291+lily-de@users.noreply.github.com> Co-authored-by: kalvinnchau <kalvin@block.xyz> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Rizel Scarlett <rizel@squareup.com> Co-authored-by: bwrage <bwrage@squareup.com> Co-authored-by: Kalvin Chau <kalvin@squareup.com> Co-authored-by: Alice Hau <110418948+ahau-square@users.noreply.github.com> Co-authored-by: Alistair Gray <ajgray@stripe.com> Co-authored-by: Nahiyan Khan <nahiyan.khan@gmail.com> Co-authored-by: Alex Hancock <alexhancock@squareup.com> Co-authored-by: Nahiyan Khan <nahiyan@squareup.com> Co-authored-by: marcelle <1852848+laanak08@users.noreply.github.com> Co-authored-by: Yingjie He <yingjiehe@block.xyz> Co-authored-by: Yingjie He <yingjiehe@squareup.com> Co-authored-by: Lily Delalande <ldelalande@block.xyz> Co-authored-by: Adewale Abati <acekyd01@gmail.com> Co-authored-by: Ebony Louis <ebony774@gmail.com> Co-authored-by: Angie Jones <jones.angie@gmail.com> Co-authored-by: Ebony Louis <55366651+EbonyLouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card } from '../../ui/card';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Input } from '../../ui/input';
|
||||
import { FullExtensionConfig } from '../../../extensions';
|
||||
import { toast } from 'react-toastify';
|
||||
import Select from 'react-select';
|
||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
|
||||
interface ManualExtensionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (extension: FullExtensionConfig) => void;
|
||||
}
|
||||
|
||||
export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtensionModalProps) {
|
||||
const [formData, setFormData] = useState<
|
||||
Partial<FullExtensionConfig> & { commandInput?: string }
|
||||
>({
|
||||
type: 'stdio',
|
||||
enabled: true,
|
||||
args: [],
|
||||
commandInput: '',
|
||||
});
|
||||
const [envKey, setEnvKey] = useState('');
|
||||
const [envValue, setEnvValue] = useState('');
|
||||
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'stdio', label: 'Standard IO' },
|
||||
{ value: 'sse', label: 'Server-Sent Events' },
|
||||
{ value: 'builtin', label: 'Built-in' },
|
||||
];
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.id || !formData.name || !formData.description) {
|
||||
toast.error('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'stdio' && !formData.commandInput) {
|
||||
toast.error('Command is required for stdio type');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'sse' && !formData.uri) {
|
||||
toast.error('URI is required for SSE type');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'builtin' && !formData.name) {
|
||||
toast.error('Name is required for builtin type');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Store environment variables as secrets
|
||||
for (const envVar of envVars) {
|
||||
const storeResponse = await fetch(getApiUrl('/secrets/store'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: envVar.key,
|
||||
value: envVar.value.trim(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!storeResponse.ok) {
|
||||
throw new Error(`Failed to store environment variable: ${envVar.key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command input into cmd and args
|
||||
let cmd = '';
|
||||
let args: string[] = [];
|
||||
if (formData.type === 'stdio' && formData.commandInput) {
|
||||
const parts = formData.commandInput.trim().split(/\s+/);
|
||||
[cmd, ...args] = parts;
|
||||
}
|
||||
|
||||
const extension: FullExtensionConfig = {
|
||||
...formData,
|
||||
type: formData.type!,
|
||||
enabled: true,
|
||||
env_keys: envVars.map((v) => v.key),
|
||||
...(formData.type === 'stdio' && { cmd, args }),
|
||||
} as FullExtensionConfig;
|
||||
|
||||
onSubmit(extension);
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error('Error configuring extension:', error);
|
||||
toast.error('Failed to configure extension');
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
type: 'stdio',
|
||||
enabled: true,
|
||||
args: [],
|
||||
commandInput: '',
|
||||
});
|
||||
setEnvVars([]);
|
||||
setEnvKey('');
|
||||
setEnvValue('');
|
||||
};
|
||||
|
||||
const handleAddEnvVar = () => {
|
||||
if (envKey && !envVars.some((v) => v.key === envKey)) {
|
||||
setEnvVars([...envVars, { key: envKey, value: envValue }]);
|
||||
setEnvKey('');
|
||||
setEnvValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveEnvVar = (key: string) => {
|
||||
setEnvVars(envVars.filter((v) => v.key !== key));
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm">
|
||||
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0">
|
||||
<div className="px-4 pb-0 space-y-8">
|
||||
<div className="flex">
|
||||
<h2 className="text-2xl font-regular text-textStandard">Add Extension Manually</h2>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">Type</label>
|
||||
<Select
|
||||
options={typeOptions}
|
||||
value={typeOptions.find((option) => option.value === formData.type)}
|
||||
onChange={(option) =>
|
||||
setFormData({ ...formData, type: option?.value as FullExtensionConfig['type'] })
|
||||
}
|
||||
styles={createDarkSelectStyles()}
|
||||
theme={darkSelectTheme}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">ID *</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.id || ''}
|
||||
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">Name *</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||
Description *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.type === 'stdio' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||
Command * (command and arguments separated by spaces)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.commandInput || ''}
|
||||
onChange={(e) => setFormData({ ...formData, commandInput: e.target.value })}
|
||||
placeholder="e.g. goosed mcp example"
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.type === 'sse' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">URI *</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.uri || ''}
|
||||
onChange={(e) => setFormData({ ...formData, uri: e.target.value })}
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||
Environment Variables
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={envKey}
|
||||
onChange={(e) => setEnvKey(e.target.value)}
|
||||
placeholder="Environment variable name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={envValue}
|
||||
onChange={(e) => setEnvValue(e.target.value)}
|
||||
placeholder="Value"
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddEnvVar}
|
||||
className="bg-bgApp hover:bg-bgApp shadow-none border border-borderSubtle hover:border-borderStandard transition-colors text-textStandard"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{envVars.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{envVars.map((envVar) => (
|
||||
<div
|
||||
key={envVar.key}
|
||||
className="flex items-center justify-between bg-gray-100 dark:bg-gray-700 p-2 rounded"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-medium">{envVar.key}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
|
||||
= {envVar.value}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveEnvVar(envVar.key)}
|
||||
className="text-red-500 hover:text-red-700 ml-2"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-[8px] -ml-8 -mr-8 pt-8">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-regular"
|
||||
>
|
||||
Add Extension
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
onClose();
|
||||
}}
|
||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user