Add all-in-one desktop wallet with extensive feature set: Infrastructure: - Storage: IPFS-based decentralized file storage with upload/download - Hosting: Domain registration and static site hosting - Compute: GPU/CPU job marketplace for distributed computing - Database: Multi-model database services (KV, Document, Vector, etc.) Financial Features: - Privacy: Confidential transactions with Pedersen commitments - Bridge: Cross-chain transfers (Ethereum, Bitcoin, IBC/Cosmos) - Governance: DAO proposals, voting, and delegation - ZK-Rollup: L2 scaling with deposits, withdrawals, and transfers UI/UX Improvements: - Add ErrorBoundary component for graceful error handling - Add LoadingStates components (spinners, skeletons, overlays) - Add Animation components (FadeIn, SlideIn, CountUp, etc.) - Update navigation with new feature sections Testing: - Add Playwright E2E smoke tests - Test route accessibility and page rendering - Verify build process and asset loading Build: - Fix TypeScript compilation errors - Update Tauri plugin dependencies - Successfully build macOS app bundle and DMG
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import {
|
||
Database,
|
||
Plus,
|
||
Trash2,
|
||
RefreshCw,
|
||
AlertCircle,
|
||
Search,
|
||
Play,
|
||
FileJson,
|
||
Key,
|
||
Clock,
|
||
GitBranch,
|
||
Table,
|
||
Braces,
|
||
} from 'lucide-react';
|
||
import { useDatabaseStore, DATABASE_TYPES, REGIONS, DatabaseType } from '../../store/database';
|
||
|
||
const TYPE_ICONS: Record<DatabaseType, React.ReactNode> = {
|
||
kv: <Key size={20} className="text-blue-400" />,
|
||
document: <FileJson size={20} className="text-green-400" />,
|
||
vector: <Braces size={20} className="text-purple-400" />,
|
||
timeseries: <Clock size={20} className="text-yellow-400" />,
|
||
graph: <GitBranch size={20} className="text-pink-400" />,
|
||
sql: <Table size={20} className="text-cyan-400" />,
|
||
};
|
||
|
||
const TYPE_DESCRIPTIONS: Record<DatabaseType, string> = {
|
||
kv: 'Fast key-value storage for caching and simple data',
|
||
document: 'JSON document storage with flexible schemas',
|
||
vector: 'Vector embeddings for AI/ML and semantic search',
|
||
timeseries: 'Time-indexed data for metrics and analytics',
|
||
graph: 'Connected data with relationships and traversals',
|
||
sql: 'Traditional relational database with ACID compliance',
|
||
};
|
||
|
||
export default function DatabaseDashboard() {
|
||
const {
|
||
instances,
|
||
isLoading,
|
||
isCreating,
|
||
error,
|
||
clearError,
|
||
fetchInstances,
|
||
createDatabase,
|
||
deleteDatabase,
|
||
executeQuery,
|
||
} = useDatabaseStore();
|
||
|
||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||
const [newDbName, setNewDbName] = useState('');
|
||
const [newDbType, setNewDbType] = useState<DatabaseType>('document');
|
||
const [newDbRegion, setNewDbRegion] = useState('us-east');
|
||
const [selectedDb, setSelectedDb] = useState<string | null>(null);
|
||
const [queryInput, setQueryInput] = useState('');
|
||
const [isQuerying, setIsQuerying] = useState(false);
|
||
const [queryResult, setQueryResult] = useState<unknown>(null);
|
||
|
||
useEffect(() => {
|
||
fetchInstances();
|
||
}, [fetchInstances]);
|
||
|
||
const handleCreateDatabase = async () => {
|
||
if (!newDbName) return;
|
||
try {
|
||
await createDatabase(newDbName, newDbType, newDbRegion);
|
||
setShowCreateForm(false);
|
||
setNewDbName('');
|
||
fetchInstances();
|
||
} catch {
|
||
// Error handled by store
|
||
}
|
||
};
|
||
|
||
const handleDeleteDatabase = async (id: string) => {
|
||
if (!confirm('Are you sure you want to delete this database? This action cannot be undone.')) {
|
||
return;
|
||
}
|
||
await deleteDatabase(id);
|
||
if (selectedDb === id) {
|
||
setSelectedDb(null);
|
||
}
|
||
fetchInstances();
|
||
};
|
||
|
||
const handleQuery = async () => {
|
||
if (!selectedDb || !queryInput) return;
|
||
setIsQuerying(true);
|
||
try {
|
||
const result = await executeQuery(selectedDb, queryInput);
|
||
setQueryResult(result);
|
||
} catch {
|
||
// Error handled by store
|
||
} finally {
|
||
setIsQuerying(false);
|
||
}
|
||
};
|
||
|
||
const formatSize = (bytes: number) => {
|
||
if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
|
||
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(2)} MB`;
|
||
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(2)} KB`;
|
||
return `${bytes} B`;
|
||
};
|
||
|
||
const selectedDatabase = instances.find((db) => db.id === selectedDb);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white">Database Services</h1>
|
||
<p className="text-gray-400 mt-1">Multi-model decentralized databases</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={fetchInstances}
|
||
disabled={isLoading}
|
||
className="flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-gray-300 transition-colors disabled:opacity-50"
|
||
>
|
||
<RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
|
||
Refresh
|
||
</button>
|
||
<button
|
||
onClick={() => setShowCreateForm(true)}
|
||
className="flex items-center gap-2 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors"
|
||
>
|
||
<Plus size={16} />
|
||
Create Database
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error Alert */}
|
||
{error && (
|
||
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
|
||
<AlertCircle className="text-red-400" size={20} />
|
||
<p className="text-red-400 flex-1">{error}</p>
|
||
<button onClick={clearError} className="text-red-400 hover:text-red-300">
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create Database Modal */}
|
||
{showCreateForm && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 w-full max-w-lg">
|
||
<h2 className="text-xl font-bold text-white mb-4">Create Database</h2>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">Database Name</label>
|
||
<input
|
||
type="text"
|
||
value={newDbName}
|
||
onChange={(e) => setNewDbName(e.target.value)}
|
||
placeholder="my-database"
|
||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-2">Database Type</label>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{DATABASE_TYPES.map((type) => (
|
||
<button
|
||
key={type.value}
|
||
onClick={() => setNewDbType(type.value)}
|
||
className={`flex items-center gap-2 p-3 rounded-lg border transition-colors ${
|
||
newDbType === type.value
|
||
? 'border-synor-500 bg-synor-600/20'
|
||
: 'border-gray-700 bg-gray-800 hover:border-gray-600'
|
||
}`}
|
||
>
|
||
{TYPE_ICONS[type.value]}
|
||
<div className="text-left">
|
||
<p className="text-white font-medium">{type.label}</p>
|
||
<p className="text-xs text-gray-500">{type.description.split(' ').slice(0, 3).join(' ')}...</p>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">Region</label>
|
||
<select
|
||
value={newDbRegion}
|
||
onChange={(e) => setNewDbRegion(e.target.value)}
|
||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
|
||
>
|
||
{REGIONS.map((region) => (
|
||
<option key={region.value} value={region.value}>
|
||
{region.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setShowCreateForm(false)}
|
||
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleCreateDatabase}
|
||
disabled={!newDbName || isCreating}
|
||
className="flex-1 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
|
||
>
|
||
{isCreating ? 'Creating...' : 'Create'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
{/* Database List */}
|
||
<div className="lg:col-span-1 space-y-4">
|
||
<h2 className="text-lg font-semibold text-white">Your Databases</h2>
|
||
{instances.map((db) => (
|
||
<div
|
||
key={db.id}
|
||
onClick={() => setSelectedDb(db.id)}
|
||
className={`bg-gray-900 rounded-xl p-4 border cursor-pointer transition-colors ${
|
||
selectedDb === db.id
|
||
? 'border-synor-500 bg-synor-600/10'
|
||
: 'border-gray-800 hover:border-gray-700'
|
||
}`}
|
||
>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
{TYPE_ICONS[db.dbType]}
|
||
<div>
|
||
<h3 className="text-white font-medium">{db.name}</h3>
|
||
<p className="text-sm text-gray-500 capitalize">{db.dbType} • {db.region}</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleDeleteDatabase(db.id);
|
||
}}
|
||
className="p-1 hover:bg-gray-800 rounded transition-colors"
|
||
>
|
||
<Trash2 size={16} className="text-gray-500 hover:text-red-400" />
|
||
</button>
|
||
</div>
|
||
<div className="mt-3 grid grid-cols-2 gap-2 text-sm">
|
||
<div>
|
||
<p className="text-gray-500">Size</p>
|
||
<p className="text-white">{formatSize(db.storageUsed)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-gray-500">Status</p>
|
||
<p className="text-white capitalize">{db.status}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{instances.length === 0 && (
|
||
<div className="text-center py-8 text-gray-500">
|
||
<Database size={32} className="mx-auto mb-2 opacity-50" />
|
||
<p>No databases yet</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Query Panel */}
|
||
<div className="lg:col-span-2">
|
||
{selectedDatabase ? (
|
||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
||
<div className="p-4 border-b border-gray-800">
|
||
<div className="flex items-center gap-3">
|
||
{TYPE_ICONS[selectedDatabase.dbType]}
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">{selectedDatabase.name}</h2>
|
||
<p className="text-sm text-gray-400">{TYPE_DESCRIPTIONS[selectedDatabase.dbType]}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="p-4 border-b border-gray-800">
|
||
<label className="block text-sm text-gray-400 mb-2">
|
||
<Search size={14} className="inline mr-1" />
|
||
Query
|
||
</label>
|
||
<textarea
|
||
value={queryInput}
|
||
onChange={(e) => setQueryInput(e.target.value)}
|
||
placeholder={
|
||
selectedDatabase.dbType === 'sql'
|
||
? 'SELECT * FROM users WHERE active = true'
|
||
: selectedDatabase.dbType === 'document'
|
||
? '{"filter": {"status": "active"}, "limit": 10}'
|
||
: selectedDatabase.dbType === 'kv'
|
||
? 'GET user:123'
|
||
: selectedDatabase.dbType === 'vector'
|
||
? '{"vector": [0.1, 0.2, ...], "topK": 10}'
|
||
: selectedDatabase.dbType === 'graph'
|
||
? 'MATCH (n:User)-[:FOLLOWS]->(m) RETURN m'
|
||
: '{"start": "2024-01-01", "end": "2024-01-31"}'
|
||
}
|
||
rows={4}
|
||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono text-sm placeholder-gray-500 focus:outline-none focus:border-synor-500"
|
||
/>
|
||
<button
|
||
onClick={handleQuery}
|
||
disabled={!queryInput || isQuerying}
|
||
className="mt-2 flex items-center gap-2 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
|
||
>
|
||
<Play size={16} />
|
||
{isQuerying ? 'Executing...' : 'Execute Query'}
|
||
</button>
|
||
</div>
|
||
<div className="p-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<label className="text-sm text-gray-400">Result</label>
|
||
{queryResult !== null && (
|
||
<button
|
||
onClick={() => setQueryResult(null)}
|
||
className="text-xs text-gray-500 hover:text-gray-400"
|
||
>
|
||
Clear
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="bg-gray-800 rounded-lg p-4 min-h-[200px] max-h-[400px] overflow-auto">
|
||
{queryResult ? (
|
||
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
|
||
{JSON.stringify(queryResult, null, 2)}
|
||
</pre>
|
||
) : (
|
||
<p className="text-gray-500 text-sm">Execute a query to see results</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-12 text-center">
|
||
<Database size={48} className="mx-auto mb-4 text-gray-600" />
|
||
<h3 className="text-lg font-medium text-white mb-2">Select a Database</h3>
|
||
<p className="text-gray-500">Choose a database from the list to query it</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|