Apply modern SaaS design to all admin pages
Modernize all admin interface pages with consistent design language: - Add hero sections with gradient backgrounds and blur effects - Implement modern card designs with hover animations - Use gradient buttons with shadow effects - Add emoji icons in colored containers - Apply consistent color themes per page - Enhance user experience with smooth transitions Pages updated: - /admin/devices (purple theme) - /admin/mqtt (cyan/blue theme) - /admin/setup (emerald theme) - /admin/users (violet theme) - /admin/settings (indigo theme) - /admin/emails (pink/rose theme) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -341,39 +341,66 @@ export default function MqttPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">MQTT Provisioning</h1>
|
||||
<div className="flex gap-4 items-center">
|
||||
{syncStatus && syncStatus.pending_changes > 0 && (
|
||||
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-md text-sm">
|
||||
{syncStatus.pending_changes} ausstehende Änderungen
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={syncing || !syncStatus || syncStatus.pending_changes === 0}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{syncing ? "Synchronisiere..." : "MQTT Sync"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Device Provisionieren
|
||||
</button>
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section with Gradient */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-cyan-600 via-blue-700 to-indigo-800 p-8 shadow-xl">
|
||||
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
|
||||
<div className="relative">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold text-white mb-2">MQTT Provisioning</h2>
|
||||
<p className="text-cyan-100 text-lg">Verwalte MQTT-Zugangsdaten und Berechtigungen</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{syncStatus && syncStatus.pending_changes > 0 && (
|
||||
<span className="px-4 py-2 bg-yellow-400 text-yellow-900 rounded-lg text-sm font-bold shadow-lg">
|
||||
{syncStatus.pending_changes} ausstehende Änderungen
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={syncing || !syncStatus || syncStatus.pending_changes === 0}
|
||||
className="px-5 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl hover:from-green-600 hover:to-emerald-700 disabled:from-gray-400 disabled:to-gray-500 font-semibold shadow-lg hover:shadow-xl transition-all"
|
||||
>
|
||||
{syncing ? "Synchronisiere..." : "🔄 MQTT Sync"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-5 py-2.5 bg-white text-cyan-700 rounded-xl hover:bg-cyan-50 font-semibold shadow-lg hover:shadow-xl transition-all"
|
||||
>
|
||||
+ Device Provisionieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{syncStatus && (
|
||||
<div className="mb-6 p-4 bg-gray-100 rounded-md">
|
||||
<h3 className="font-semibold mb-2">Sync Status</h3>
|
||||
<div className="text-sm">
|
||||
<p>Status: <span className={syncStatus.last_sync_status === 'success' ? 'text-green-600' : 'text-red-600'}>{syncStatus.last_sync_status}</span></p>
|
||||
{syncStatus.last_sync_at && (
|
||||
<p>Letzter Sync: {new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}</p>
|
||||
)}
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-slate-50 to-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-slate-600 to-slate-700 flex items-center justify-center text-white text-xl">
|
||||
🔄
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900">Sync Status</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-4 rounded-xl border border-blue-100">
|
||||
<p className="text-sm font-semibold text-blue-700 uppercase tracking-wide mb-1">Status</p>
|
||||
<p className={`text-2xl font-bold ${syncStatus.last_sync_status === 'success' ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{syncStatus.last_sync_status === 'success' ? '✓ Erfolgreich' : '✗ Fehler'}
|
||||
</p>
|
||||
</div>
|
||||
{syncStatus.last_sync_at && (
|
||||
<div className="bg-gradient-to-br from-purple-50 to-violet-50 p-4 rounded-xl border border-purple-100">
|
||||
<p className="text-sm font-semibold text-purple-700 uppercase tracking-wide mb-1">Letzter Sync</p>
|
||||
<p className="text-lg font-bold text-purple-900">{new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -383,26 +410,48 @@ export default function MqttPage() {
|
||||
const deviceRules = aclRules[cred.device_id] || [];
|
||||
|
||||
return (
|
||||
<div key={cred.id} className="border rounded-lg p-6 bg-white shadow-sm">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{cred.device_name}</h3>
|
||||
<p className="text-sm text-gray-500">Device ID: {cred.device_id}</p>
|
||||
<p className="text-sm text-gray-600 mt-1">Username: <code className="bg-gray-100 px-2 py-1 rounded">{cred.mqtt_username}</code></p>
|
||||
<p className="text-xs text-gray-500 mt-1">Erstellt: {new Date(cred.created_at).toLocaleString('de-DE')}</p>
|
||||
<div key={cred.id} className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-2xl transition-shadow">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-5 border-b border-blue-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center text-white text-2xl shadow-lg">
|
||||
📱
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-900">{cred.device_name}</h3>
|
||||
<p className="text-sm text-gray-600">Device ID: <code className="bg-white px-2 py-0.5 rounded font-mono text-xs">{cred.device_id}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleEnabled(cred.device_id, !cred.enabled)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-bold shadow-md ${cred.enabled ? 'bg-gradient-to-r from-green-500 to-emerald-600 text-white' : 'bg-gradient-to-r from-red-500 to-red-600 text-white'}`}
|
||||
>
|
||||
{cred.enabled ? '✓ Aktiviert' : '✗ Deaktiviert'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleEnabled(cred.device_id, !cred.enabled)}
|
||||
className={`px-3 py-1 rounded text-sm ${cred.enabled ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}
|
||||
>
|
||||
{cred.enabled ? 'Aktiviert' : 'Deaktiviert'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-xl p-4 mb-4 border border-gray-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Username</p>
|
||||
<code className="text-sm font-mono text-gray-900 bg-white px-3 py-1.5 rounded-lg border border-gray-200 inline-block">{cred.mqtt_username}</code>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Erstellt</p>
|
||||
<p className="text-sm text-gray-700">{new Date(cred.created_at).toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => handleRegeneratePassword(cred.device_id)}
|
||||
className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded text-sm hover:bg-yellow-200"
|
||||
className="flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-lg hover:from-yellow-600 hover:to-orange-600 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
Passwort Reset
|
||||
🔑 Passwort Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -414,60 +463,61 @@ export default function MqttPage() {
|
||||
});
|
||||
setShowAclModal(true);
|
||||
}}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded text-sm hover:bg-blue-200"
|
||||
className="flex-1 px-4 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
ACL Hinzufügen
|
||||
+ ACL Hinzufügen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCredentials(cred.device_id)}
|
||||
className="px-3 py-1 bg-red-100 text-red-800 rounded text-sm hover:bg-red-200"
|
||||
className="px-4 py-2.5 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg hover:from-red-700 hover:to-red-800 text-sm font-semibold shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
Löschen
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="font-semibold mb-2 text-sm">ACL Regeln:</h4>
|
||||
<div>
|
||||
<h4 className="font-bold mb-3 text-gray-900 flex items-center gap-2">
|
||||
<span className="text-lg">🔐</span>
|
||||
ACL Regeln
|
||||
</h4>
|
||||
{deviceRules.length > 0 ? (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Topic Pattern</th>
|
||||
<th className="px-4 py-2 text-left">Berechtigung</th>
|
||||
<th className="px-4 py-2 text-left">Erstellt</th>
|
||||
<th className="px-4 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{deviceRules.map(rule => (
|
||||
<tr key={rule.id} className="border-t">
|
||||
<td className="px-4 py-2"><code className="bg-gray-100 px-2 py-1 rounded">{rule.topic_pattern}</code></td>
|
||||
<td className="px-4 py-2"><span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{rule.permission}</span></td>
|
||||
<td className="px-4 py-2 text-gray-500">{new Date(rule.created_at).toLocaleString('de-DE')}</td>
|
||||
<td className="px-4 py-2">
|
||||
<button
|
||||
onClick={() => handleDeleteAclRule(rule.id, cred.device_id)}
|
||||
className="text-red-600 hover:text-red-800 text-xs"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="space-y-2">
|
||||
{deviceRules.map(rule => (
|
||||
<div key={rule.id} className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<code className="text-sm font-mono bg-white px-3 py-1.5 rounded-lg border border-gray-200">{rule.topic_pattern}</code>
|
||||
<span className="px-3 py-1 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-lg text-xs font-bold">{rule.permission}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Erstellt: {new Date(rule.created_at).toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteAclRule(rule.id, cred.device_id)}
|
||||
className="ml-4 px-3 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 text-xs font-semibold"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Keine ACL Regeln definiert</p>
|
||||
<div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-lg p-6 text-center border border-gray-200">
|
||||
<p className="text-sm text-gray-500">Keine ACL Regeln definiert</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{credentials.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
Noch keine Devices provisioniert
|
||||
<div className="relative overflow-hidden bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl shadow-lg p-12 text-center border border-gray-200">
|
||||
<div className="absolute top-0 right-0 text-9xl opacity-5">🔐</div>
|
||||
<p className="text-xl font-semibold text-gray-600 mb-2">Noch keine Devices provisioniert</p>
|
||||
<p className="text-gray-500">Erstelle MQTT-Credentials für dein erstes Device.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user