ldelalande+alexhancock/shared-config-client (#1219)
Co-authored-by: Lily Delalande <ldelalande@squareup.com>
This commit is contained in:
@@ -9,6 +9,8 @@ release-binary:
|
|||||||
@echo "Building release version..."
|
@echo "Building release version..."
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@just copy-binary
|
@just copy-binary
|
||||||
|
@echo "Generating OpenAPI schema..."
|
||||||
|
cargo run -p goose-server --bin generate_schema
|
||||||
|
|
||||||
# Build Windows executable
|
# Build Windows executable
|
||||||
release-windows:
|
release-windows:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
out
|
out
|
||||||
|
src/api/generated
|
||||||
*.lock
|
*.lock
|
||||||
@@ -37,6 +37,7 @@ const noWindowLocationHref = {
|
|||||||
module.exports = [
|
module.exports = [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
{
|
{
|
||||||
|
ignores: ['src/api/generated/**'],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from '@hey-api/openapi-ts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: './openapi.json',
|
||||||
|
output: './src/api/generated',
|
||||||
|
plugins: ['@hey-api/client-fetch'],
|
||||||
|
});
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "goose-server",
|
||||||
|
"description": "An AI agent",
|
||||||
|
"contact": {
|
||||||
|
"name": "Block",
|
||||||
|
"email": "ai-oss-tools@block.xyz"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"version": "1.0.5"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/config": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "read_all_config",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "All configuration values retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConfigResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/config/extension": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "add_extension",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ExtensionQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Extension added successfully",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "remove_extension",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConfigKeyQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Extension removed successfully",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Extension not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/config/read": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "read_config",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConfigKeyQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Configuration value retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Configuration key not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/config/remove": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "remove_config",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConfigKeyQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Configuration value removed successfully",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Configuration key not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/config/upsert": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::config_management"
|
||||||
|
],
|
||||||
|
"operationId": "upsert_config",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpsertConfigQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Configuration value upserted successfully",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ConfigKeyQuery": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConfigResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"config"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ExtensionQuery": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"config"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"config": {},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpsertConfigQuery": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"key",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"is_secret": {
|
||||||
|
"type": "boolean",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2711
-1115
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,8 @@
|
|||||||
"description": "Goose App",
|
"description": "Goose App",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start-gui": "electron-forge start",
|
"generate-api": "openapi-ts",
|
||||||
|
"start-gui": "npm run generate-api && electron-forge start",
|
||||||
"start": "cd ../.. && just run-ui",
|
"start": "cd ../.. && just run-ui",
|
||||||
"start:test-error": "GOOSE_TEST_ERROR=true electron-forge start",
|
"start:test-error": "GOOSE_TEST_ERROR=true electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"@electron-forge/plugin-vite": "^7.5.0",
|
"@electron-forge/plugin-vite": "^7.5.0",
|
||||||
"@electron/fuses": "^1.8.0",
|
"@electron/fuses": "^1.8.0",
|
||||||
"@eslint/js": "^8.56.0",
|
"@eslint/js": "^8.56.0",
|
||||||
|
"@hey-api/openapi-ts": "^0.64.4",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/electron-squirrel-startup": "^1.0.2",
|
"@types/electron-squirrel-startup": "^1.0.2",
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.72",
|
"@ai-sdk/openai": "^0.0.72",
|
||||||
"@ai-sdk/ui-utils": "^1.0.2",
|
"@ai-sdk/ui-utils": "^1.0.2",
|
||||||
|
"@hey-api/client-fetch": "^0.8.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import Splash from './components/Splash';
|
|||||||
import Settings from './components/settings/Settings';
|
import Settings from './components/settings/Settings';
|
||||||
import MoreModelsSettings from './components/settings/models/MoreModels';
|
import MoreModelsSettings from './components/settings/models/MoreModels';
|
||||||
import ConfigureProviders from './components/settings/providers/ConfigureProviders';
|
import ConfigureProviders from './components/settings/providers/ConfigureProviders';
|
||||||
|
import { ConfigPage } from './components/pages/ConfigPage';
|
||||||
|
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -35,7 +36,13 @@ export interface Chat {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type View = 'welcome' | 'chat' | 'settings' | 'moreModels' | 'configureProviders';
|
export type View =
|
||||||
|
| 'welcome'
|
||||||
|
| 'chat'
|
||||||
|
| 'settings'
|
||||||
|
| 'moreModels'
|
||||||
|
| 'configureProviders'
|
||||||
|
| 'configPage';
|
||||||
|
|
||||||
// This component is our main chat content.
|
// This component is our main chat content.
|
||||||
// We'll move the majority of chat logic here, minus the 'view' state.
|
// We'll move the majority of chat logic here, minus the 'view' state.
|
||||||
@@ -388,6 +395,14 @@ export default function ChatWindow() {
|
|||||||
setView={setView}
|
setView={setView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{view === 'configPage' && (
|
||||||
|
<ConfigPage
|
||||||
|
onClose={() => {
|
||||||
|
setView('chat');
|
||||||
|
}}
|
||||||
|
setView={setView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{view === 'configureProviders' && (
|
{view === 'configureProviders' && (
|
||||||
<ConfigureProviders
|
<ConfigureProviders
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button } from './components/ui/button';
|
||||||
|
import { Input } from './components/ui/input';
|
||||||
|
import { Label } from './components/ui/label';
|
||||||
|
import { Card } from './components/ui/card';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import BackButton from './components/ui/BackButton';
|
||||||
|
import { useConfig } from './hooks/useConfig';
|
||||||
|
|
||||||
|
export default function ConfigPage() {
|
||||||
|
const [configs, setConfigs] = useState<Record<string, any>>({});
|
||||||
|
const [newKey, setNewKey] = useState('');
|
||||||
|
const [newValue, setNewValue] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { loading, error, loadConfigs, addConfig, removeConfig } = useConfig();
|
||||||
|
|
||||||
|
// Fetch all configs on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
const result = await loadConfigs();
|
||||||
|
setConfigs(result);
|
||||||
|
};
|
||||||
|
fetchConfigs();
|
||||||
|
}, [loadConfigs]);
|
||||||
|
|
||||||
|
const handleAddConfig = async () => {
|
||||||
|
if (!newKey || !newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedValue = newValue;
|
||||||
|
// Try to parse as JSON if it looks like JSON
|
||||||
|
if (newValue.trim().startsWith('{') || newValue.trim().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
parsedValue = JSON.parse(newValue);
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, use the original string value
|
||||||
|
console.log('Value is not valid JSON, using as string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await addConfig(newKey, parsedValue);
|
||||||
|
if (success) {
|
||||||
|
setNewKey('');
|
||||||
|
setNewValue('');
|
||||||
|
const updatedConfigs = await loadConfigs();
|
||||||
|
setConfigs(updatedConfigs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveConfig = async (key: string) => {
|
||||||
|
const success = await removeConfig(key);
|
||||||
|
if (success) {
|
||||||
|
const updatedConfigs = await loadConfigs();
|
||||||
|
setConfigs(updatedConfigs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-full">
|
||||||
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col pb-24">
|
||||||
|
<div className="px-8 pt-6 pb-4">
|
||||||
|
<BackButton onClick={() => navigate('/settings')} />
|
||||||
|
<h1 className="text-3xl font-medium text-textStandard mt-1">Configuration</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 py-8 pt-[20px] px-8">
|
||||||
|
<div className="space-y-8 max-w-2xl">
|
||||||
|
{/* Add new config form */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Add New Configuration</h2>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="configKey">Key</Label>
|
||||||
|
<Input
|
||||||
|
id="configKey"
|
||||||
|
value={newKey}
|
||||||
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
|
placeholder="Enter config key"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="configValue">Value</Label>
|
||||||
|
<Input
|
||||||
|
id="configValue"
|
||||||
|
value={newValue}
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
placeholder="Enter config value (string or JSON)"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAddConfig} disabled={loading}>
|
||||||
|
{loading ? 'Adding...' : 'Add Configuration'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Error display */}
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Config list */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Current Configurations</h2>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-gray-500">Loading configurations...</div>
|
||||||
|
) : Object.keys(configs).length === 0 ? (
|
||||||
|
<div className="text-center text-gray-500">No configurations found</div>
|
||||||
|
) : (
|
||||||
|
Object.entries(configs).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-800 rounded"
|
||||||
|
>
|
||||||
|
<div className="break-all">
|
||||||
|
<span className="font-medium">{key}:</span>{' '}
|
||||||
|
<span className="text-gray-600 dark:text-gray-300">
|
||||||
|
{typeof value === 'object'
|
||||||
|
? JSON.stringify(value, null, 2)
|
||||||
|
: String(value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleRemoveConfig(key)}
|
||||||
|
size="sm"
|
||||||
|
className="ml-4 shrink-0"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '...' : 'Remove'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
readAllConfig,
|
||||||
|
readConfig,
|
||||||
|
removeConfig,
|
||||||
|
upsertConfig,
|
||||||
|
addExtension,
|
||||||
|
removeExtension,
|
||||||
|
} from './generated';
|
||||||
|
import { client } from './generated/client.gen';
|
||||||
|
|
||||||
|
// Initialize client configuration
|
||||||
|
client.setConfig({
|
||||||
|
baseUrl: window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': window.appConfig.get('secretKey'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export class Config {
|
||||||
|
static async upsert(key: string, value: any, isSecret?: boolean) {
|
||||||
|
return await upsertConfig({
|
||||||
|
body: {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
is_secret: isSecret,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async read(key: string) {
|
||||||
|
return await readConfig({
|
||||||
|
body: { key },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(key: string) {
|
||||||
|
return await removeConfig({
|
||||||
|
body: { key },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readAll() {
|
||||||
|
const response = await readAllConfig();
|
||||||
|
return response.data.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addExtension(name: string, config: any) {
|
||||||
|
return await addExtension({
|
||||||
|
body: { name, config },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeExtension(name: string) {
|
||||||
|
return await removeExtension({
|
||||||
|
body: { key: name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { ClientOptions } from './types.gen';
|
||||||
|
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
* and the returned object will become the client's initial configuration.
|
||||||
|
*
|
||||||
|
* You may want to initialize your client this way instead of calling
|
||||||
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
|
* to ensure your client always has the correct values.
|
||||||
|
*/
|
||||||
|
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
|
||||||
|
|
||||||
|
export const client = createClient(createConfig<ClientOptions>());
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
export * from './types.gen';
|
||||||
|
export * from './sdk.gen';
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
|
||||||
|
import type { ReadAllConfigData, ReadAllConfigResponse, RemoveExtensionData, RemoveExtensionResponse, AddExtensionData, AddExtensionResponse, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen';
|
||||||
|
import { client as _heyApiClient } from './client.gen';
|
||||||
|
|
||||||
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||||
|
/**
|
||||||
|
* You can provide a client instance returned by `createClient()` instead of
|
||||||
|
* individual options. This might be also useful if you want to implement a
|
||||||
|
* custom client.
|
||||||
|
*/
|
||||||
|
client?: Client;
|
||||||
|
/**
|
||||||
|
* You can pass arbitrary values through the `meta` object. This can be
|
||||||
|
* used to access values that aren't defined as part of the SDK function.
|
||||||
|
*/
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readAllConfig = <ThrowOnError extends boolean = false>(options?: Options<ReadAllConfigData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? _heyApiClient).get<ReadAllConfigResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/config',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeExtension = <ThrowOnError extends boolean = false>(options: Options<RemoveExtensionData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).delete<RemoveExtensionResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/config/extension',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addExtension = <ThrowOnError extends boolean = false>(options: Options<AddExtensionData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<AddExtensionResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/config/extension',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readConfig = <ThrowOnError extends boolean = false>(options: Options<ReadConfigData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).get<unknown, unknown, ThrowOnError>({
|
||||||
|
url: '/config/read',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeConfig = <ThrowOnError extends boolean = false>(options: Options<RemoveConfigData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<RemoveConfigResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/config/remove',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertConfig = <ThrowOnError extends boolean = false>(options: Options<UpsertConfigData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<UpsertConfigResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/config/upsert',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
export type ConfigKeyQuery = {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigResponse = {
|
||||||
|
config: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionQuery = {
|
||||||
|
config: unknown;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertConfigQuery = {
|
||||||
|
is_secret?: boolean | null;
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadAllConfigData = {
|
||||||
|
body?: never;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadAllConfigResponses = {
|
||||||
|
/**
|
||||||
|
* All configuration values retrieved successfully
|
||||||
|
*/
|
||||||
|
200: ConfigResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadAllConfigResponse = ReadAllConfigResponses[keyof ReadAllConfigResponses];
|
||||||
|
|
||||||
|
export type RemoveExtensionData = {
|
||||||
|
body: ConfigKeyQuery;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config/extension';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveExtensionErrors = {
|
||||||
|
/**
|
||||||
|
* Extension not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveExtensionResponses = {
|
||||||
|
/**
|
||||||
|
* Extension removed successfully
|
||||||
|
*/
|
||||||
|
200: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses];
|
||||||
|
|
||||||
|
export type AddExtensionData = {
|
||||||
|
body: ExtensionQuery;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config/extension';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddExtensionErrors = {
|
||||||
|
/**
|
||||||
|
* Invalid request
|
||||||
|
*/
|
||||||
|
400: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddExtensionResponses = {
|
||||||
|
/**
|
||||||
|
* Extension added successfully
|
||||||
|
*/
|
||||||
|
200: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddExtensionResponse = AddExtensionResponses[keyof AddExtensionResponses];
|
||||||
|
|
||||||
|
export type ReadConfigData = {
|
||||||
|
body: ConfigKeyQuery;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config/read';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadConfigErrors = {
|
||||||
|
/**
|
||||||
|
* Configuration key not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadConfigResponses = {
|
||||||
|
/**
|
||||||
|
* Configuration value retrieved successfully
|
||||||
|
*/
|
||||||
|
200: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveConfigData = {
|
||||||
|
body: ConfigKeyQuery;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config/remove';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveConfigErrors = {
|
||||||
|
/**
|
||||||
|
* Configuration key not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveConfigResponses = {
|
||||||
|
/**
|
||||||
|
* Configuration value removed successfully
|
||||||
|
*/
|
||||||
|
200: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveConfigResponse = RemoveConfigResponses[keyof RemoveConfigResponses];
|
||||||
|
|
||||||
|
export type UpsertConfigData = {
|
||||||
|
body: UpsertConfigQuery;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/config/upsert';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertConfigErrors = {
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertConfigResponses = {
|
||||||
|
/**
|
||||||
|
* Configuration value upserted successfully
|
||||||
|
*/
|
||||||
|
200: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertConfigResponse = UpsertConfigResponses[keyof UpsertConfigResponses];
|
||||||
|
|
||||||
|
export type ClientOptions = {
|
||||||
|
baseUrl: `${string}://${string}` | (string & {});
|
||||||
|
};
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import { Label } from '../ui/label';
|
||||||
|
import { Card } from '../ui/card';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import BackButton from '../ui/BackButton';
|
||||||
|
import { useConfig } from '../../hooks/useConfig';
|
||||||
|
import type { View } from '@/src/ChatWindow';
|
||||||
|
|
||||||
|
interface ConfigItem {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigPage({
|
||||||
|
onClose,
|
||||||
|
setView,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
setView?: (view: View) => void;
|
||||||
|
}) {
|
||||||
|
const [configs, setConfigs] = useState<Record<string, any>>({});
|
||||||
|
const [newKey, setNewKey] = useState('');
|
||||||
|
const [newValue, setNewValue] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { loading, error, loadConfigs, addConfig, removeConfig } = useConfig();
|
||||||
|
|
||||||
|
// Fetch all configs on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
const result = await loadConfigs();
|
||||||
|
setConfigs(result);
|
||||||
|
};
|
||||||
|
fetchConfigs();
|
||||||
|
}, [loadConfigs]);
|
||||||
|
|
||||||
|
const handleAddConfig = async () => {
|
||||||
|
if (!newKey || !newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedValue = newValue;
|
||||||
|
// Try to parse as JSON if it looks like JSON
|
||||||
|
if (newValue.trim().startsWith('{') || newValue.trim().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
parsedValue = JSON.parse(newValue);
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, use the original string value
|
||||||
|
console.log('Value is not valid JSON, using as string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await addConfig(newKey, parsedValue);
|
||||||
|
if (success) {
|
||||||
|
setNewKey('');
|
||||||
|
setNewValue('');
|
||||||
|
const updatedConfigs = await loadConfigs();
|
||||||
|
setConfigs(updatedConfigs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveConfig = async (key: string) => {
|
||||||
|
const success = await removeConfig(key);
|
||||||
|
if (success) {
|
||||||
|
const updatedConfigs = await loadConfigs();
|
||||||
|
setConfigs(updatedConfigs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-full">
|
||||||
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col pb-24">
|
||||||
|
<div className="px-8 pt-6 pb-4">
|
||||||
|
<BackButton onClick={() => navigate('/settings')} />
|
||||||
|
<h1 className="text-3xl font-medium text-textStandard mt-1">Configuration</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 py-8 pt-[20px] px-8">
|
||||||
|
<div className="space-y-8 max-w-2xl">
|
||||||
|
{/* Add new config form */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Add New Configuration</h2>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="configKey">Key</Label>
|
||||||
|
<Input
|
||||||
|
id="configKey"
|
||||||
|
value={newKey}
|
||||||
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
|
placeholder="Enter config key"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="configValue">Value</Label>
|
||||||
|
<Input
|
||||||
|
id="configValue"
|
||||||
|
value={newValue}
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
placeholder="Enter config value (string or JSON)"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAddConfig} disabled={loading}>
|
||||||
|
{loading ? 'Adding...' : 'Add Configuration'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Error display */}
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Config list */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Current Configurations</h2>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-gray-500">Loading configurations...</div>
|
||||||
|
) : Object.keys(configs).length === 0 ? (
|
||||||
|
<div className="text-center text-gray-500">No configurations found</div>
|
||||||
|
) : (
|
||||||
|
Object.entries(configs).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-800 rounded"
|
||||||
|
>
|
||||||
|
<div className="break-all">
|
||||||
|
<span className="font-medium">{key}:</span>{' '}
|
||||||
|
<span className="text-gray-600 dark:text-gray-300">
|
||||||
|
{typeof value === 'object'
|
||||||
|
? JSON.stringify(value, null, 2)
|
||||||
|
: String(value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleRemoveConfig(key)}
|
||||||
|
size="sm"
|
||||||
|
className="ml-4 shrink-0"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '...' : 'Remove'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -181,6 +181,8 @@ export default function Settings({
|
|||||||
return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId);
|
return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function navigate(s: string) {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
@@ -259,6 +261,27 @@ export default function Settings({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="configuration">
|
||||||
|
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
||||||
|
<h2 className="text-xl font-semibold text-textStandard">Configuration</h2>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/settings/config')}
|
||||||
|
className="text-indigo-500 hover:text-indigo-600 text-sm"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-8">
|
||||||
|
<p className="text-sm text-textStandard mb-4">
|
||||||
|
Manage application configuration and settings. You can view, add, edit, and
|
||||||
|
remove configuration values.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ConfigAPI, ConfigResponse } from './api';
|
||||||
|
|
||||||
|
export const ConfigManager: React.FC = () => {
|
||||||
|
const [config, setConfig] = useState<ConfigResponse | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [newKey, setNewKey] = useState('');
|
||||||
|
const [newValue, setNewValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const data = await ConfigAPI.readAllConfig();
|
||||||
|
setConfig(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load configuration');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpsert = async () => {
|
||||||
|
try {
|
||||||
|
await ConfigAPI.upsertConfig({
|
||||||
|
key: newKey,
|
||||||
|
value: newValue as any, // You might want to add proper parsing here
|
||||||
|
});
|
||||||
|
await loadConfig();
|
||||||
|
setNewKey('');
|
||||||
|
setNewValue('');
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to update configuration');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async (key: string) => {
|
||||||
|
try {
|
||||||
|
await ConfigAPI.removeConfig(key);
|
||||||
|
await loadConfig();
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to remove configuration');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Configuration Manager</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Add/Update Configuration</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newKey}
|
||||||
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
|
placeholder="Key"
|
||||||
|
className="border p-2 rounded"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newValue}
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
placeholder="Value"
|
||||||
|
className="border p-2 rounded"
|
||||||
|
/>
|
||||||
|
<button onClick={handleUpsert} className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Current Configuration</h3>
|
||||||
|
{config && (
|
||||||
|
<div className="border rounded">
|
||||||
|
{Object.entries(config.config).map(([key, value]) => (
|
||||||
|
<div key={key} className="p-2 border-b flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{key}:</span> <span>{JSON.stringify(value)}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemove(key)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Value } from 'yaml';
|
||||||
|
|
||||||
|
export interface UpsertConfigQuery {
|
||||||
|
key: string;
|
||||||
|
value: Value;
|
||||||
|
isSecret?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigKeyQuery {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionQuery {
|
||||||
|
name: string;
|
||||||
|
config: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigResponse {
|
||||||
|
config: Record<string, Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE = 'http://localhost:3000'; // Update this with your actual API base URL
|
||||||
|
|
||||||
|
export class ConfigAPI {
|
||||||
|
static async readAllConfig(): Promise<ConfigResponse> {
|
||||||
|
const response = await fetch(`${API_BASE}/config`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch config');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async upsertConfig(query: UpsertConfigQuery): Promise<string> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
key: query.key,
|
||||||
|
value: JSON.stringify(query.value),
|
||||||
|
...(query.isSecret && { is_secret: String(query.isSecret) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/config/upsert?${params}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to upsert config');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeConfig(key: string): Promise<string> {
|
||||||
|
const params = new URLSearchParams({ key });
|
||||||
|
const response = await fetch(`${API_BASE}/config/remove?${params}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to remove config');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readConfig(key: string): Promise<Value> {
|
||||||
|
const params = new URLSearchParams({ key });
|
||||||
|
const response = await fetch(`${API_BASE}/config/read?${params}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to read config');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addExtension(extension: ExtensionQuery): Promise<string> {
|
||||||
|
const response = await fetch(`${API_BASE}/config/extension`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(extension),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add extension');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeExtension(name: string): Promise<string> {
|
||||||
|
const params = new URLSearchParams({ key: name });
|
||||||
|
const response = await fetch(`${API_BASE}/config/extension?${params}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to remove extension');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Config } from '../api/config';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export interface UseConfigOptions {
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
showToasts?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfig(options: UseConfigOptions = {}) {
|
||||||
|
const { onError, showToasts = true } = options;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const handleError = useCallback(
|
||||||
|
(error: Error, message: string) => {
|
||||||
|
setError(error);
|
||||||
|
if (showToasts) {
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onError, showToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadConfigs = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const configs = await Config.readAll();
|
||||||
|
return configs;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error('Failed to load configurations');
|
||||||
|
handleError(error, 'Failed to load configurations');
|
||||||
|
return {};
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
const addConfig = useCallback(
|
||||||
|
async (key: string, value: any) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
await Config.upsert(key, value);
|
||||||
|
if (showToasts) {
|
||||||
|
toast.success(`Successfully added configuration: ${key}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error('Failed to add configuration');
|
||||||
|
handleError(error, `Failed to add configuration: ${key}`);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleError, showToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeConfig = useCallback(
|
||||||
|
async (key: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
await Config.remove(key);
|
||||||
|
if (showToasts) {
|
||||||
|
toast.success(`Successfully removed configuration: ${key}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error('Failed to remove configuration');
|
||||||
|
handleError(error, `Failed to remove configuration: ${key}`);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleError, showToasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const readConfig = useCallback(
|
||||||
|
async (key: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const value = await Config.read(key);
|
||||||
|
return value;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error('Failed to read configuration');
|
||||||
|
handleError(error, `Failed to read configuration: ${key}`);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleError]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
loadConfigs,
|
||||||
|
addConfig,
|
||||||
|
removeConfig,
|
||||||
|
readConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user