synor/apps/desktop-wallet/src/pages/Database/DatabaseDashboard.tsx
Gulshan Yadav 81347ab15d feat(wallet): add comprehensive desktop wallet features
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
2026-02-02 11:35:21 +05:30

350 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}