feat: enhance VPS management interface with search, filter, and toast notifications
All checks were successful
Build and Push Docker Images / docker (push) Successful in 42s

- Added a DashboardToolbar component for searching, filtering, and sorting VPS instances.
- Implemented a ConfirmDialog component for destructive actions confirmation.
- Introduced a Modal component for displaying modals with focus management.
- Created a Toast component for displaying notifications with different types (success, error, info).
- Refactored VpsCard to utilize new Metric component for displaying CPU and RAM usage.
- Improved user experience with local storage for collapsed state in VpsCard.
- Added clipboard utility functions for copying text and downloading content.
- Enhanced CSS styles for better dark mode support and animations.
- Updated various UI controls for consistency and improved accessibility.
This commit is contained in:
jeanotx32
2026-08-01 01:22:45 -04:00
parent 022fbabe5d
commit f37b639226
20 changed files with 1689 additions and 727 deletions

View File

@@ -1,10 +1,13 @@
import { useState, useEffect, useCallback } from 'react'
import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X, Database, Trash2, AlertTriangle, Fingerprint, Key, Bell, Send, Eye, EyeOff, Activity } from 'lucide-react'
import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X, Database, Trash2, Fingerprint, Key, Bell, Send, Eye, EyeOff, Activity } from 'lucide-react'
import { getAdminSettings, setAdminSetting, getLoginLogs, getDbInfo, purgeDb, adminGetPasskeys, adminDeletePasskey, getContainerEvents, testPushoverNotification } from '../api/client'
import ConfirmDialog from './ui/ConfirmDialog'
import { inputClass } from './ui/controls'
const PAGE_SIZE = 50
function ToggleRow({ label, description, enabled, onChange, loading }) { return (
function ToggleRow({ label, description, enabled, onChange, loading }) {
return (
<div className="flex items-center justify-between gap-4 py-3">
<div>
<p className="text-sm font-medium text-gray-200">{label}</p>
@@ -13,6 +16,9 @@ function ToggleRow({ label, description, enabled, onChange, loading }) { return
<button
onClick={onChange}
disabled={loading}
role="switch"
aria-checked={enabled}
aria-label={label}
title={enabled ? 'Désactiver' : 'Activer'}
className="flex-shrink-0 disabled:opacity-50 transition-opacity"
>
@@ -264,8 +270,9 @@ export default function AdminPage({ onBack }) {
const [purgeError, setPurgeError] = useState(null)
const [confirmState, setConfirmState] = useState(null) // { table, period, fromTs?, toTs? }
// Custom range
const [customFrom, setCustomFrom] = useState('')
const [customTo, setCustomTo] = useState('')
const [customFrom, setCustomFrom] = useState('')
const [customTo, setCustomTo] = useState('')
const [customTable, setCustomTable] = useState('all')
const loadDbInfo = useCallback(async () => {
setDbInfoLoading(true)
@@ -350,7 +357,7 @@ export default function AdminPage({ onBack }) {
</div>
{/* ── Tabs ── */}
<div className="flex gap-1 mb-8 border-b border-gray-800">
<div className="flex gap-1 mb-8 border-b border-gray-800 overflow-x-auto" role="tablist">
{[
{ key: 'settings', label: 'Paramètres' },
{ key: 'notifications', label: 'Notifications' },
@@ -361,7 +368,9 @@ export default function AdminPage({ onBack }) {
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
role="tab"
aria-selected={activeTab === tab.key}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
activeTab === tab.key
? 'border-indigo-500 text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-300'
@@ -446,18 +455,20 @@ export default function AdminPage({ onBack }) {
{/* Credentials form */}
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1.5">App Token</label>
<label htmlFor="pushover-token" className="block text-xs text-gray-400 mb-1.5">App Token</label>
<div className="relative">
<input
id="pushover-token"
type={showToken ? 'text' : 'password'}
value={pushoverToken}
onChange={e => setPushoverToken(e.target.value)}
placeholder="aXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 pr-9 text-xs font-mono focus:outline-none focus:border-indigo-500 transition-colors"
className={`${inputClass} pr-9 font-mono`}
/>
<button
type="button"
onClick={() => setShowToken(v => !v)}
aria-label={showToken ? 'Masquer le token' : 'Afficher le token'}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
>
{showToken ? <EyeOff size={13} /> : <Eye size={13} />}
@@ -465,18 +476,20 @@ export default function AdminPage({ onBack }) {
</div>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1.5">User Key</label>
<label htmlFor="pushover-user-key" className="block text-xs text-gray-400 mb-1.5">User Key</label>
<div className="relative">
<input
id="pushover-user-key"
type={showUserKey ? 'text' : 'password'}
value={pushoverUserKey}
onChange={e => setPushoverUserKey(e.target.value)}
placeholder="uXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 pr-9 text-xs font-mono focus:outline-none focus:border-indigo-500 transition-colors"
className={`${inputClass} pr-9 font-mono`}
/>
<button
type="button"
onClick={() => setShowUserKey(v => !v)}
aria-label={showUserKey ? 'Masquer la clé' : 'Afficher la clé'}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
>
{showUserKey ? <EyeOff size={13} /> : <Eye size={13} />}
@@ -908,10 +921,11 @@ export default function AdminPage({ onBack }) {
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Table</label>
<label htmlFor="custom-table" className="block text-xs text-gray-500 mb-1">Table</label>
<select
id="custom-table"
defaultValue="all"
value={customTable}
onChange={e => setCustomTable(e.target.value)}
className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-indigo-500 transition-colors"
>
<option value="all">Toutes</option>
@@ -922,10 +936,9 @@ export default function AdminPage({ onBack }) {
<button
disabled={!customFrom || !customTo || purgeLoading}
onClick={() => {
const tbl = document.getElementById('custom-table').value
const fromTs = Math.floor(new Date(customFrom).getTime() / 1000)
const toTs = Math.floor(new Date(customTo).getTime() / 1000)
requestPurge(tbl, 'custom', { fromTs, toTs })
requestPurge(customTable, 'custom', { fromTs, toTs })
}}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs bg-red-950/50 hover:bg-red-900/60 text-red-400 border border-red-800/50 transition-colors disabled:opacity-50"
>
@@ -939,38 +952,22 @@ export default function AdminPage({ onBack }) {
{/* ── Modale de confirmation de purge ── */}
{confirmState && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-gray-900 border border-gray-700 rounded-2xl p-6 max-w-sm w-full shadow-2xl">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-xl bg-red-500/15">
<AlertTriangle size={18} className="text-red-400" />
</div>
<h3 className="text-sm font-semibold">Confirmer la suppression</h3>
</div>
<p className="text-xs text-gray-400 mb-6">
<ConfirmDialog
danger
title="Confirmer la suppression"
message={
<>
Vous êtes sur le point de supprimer{' '}
<span className="text-gray-200 font-medium">{periodLabel[confirmState.period]}</span>{' '}
dans{' '}
<span className="text-gray-200 font-medium">{tableLabel[confirmState.table]}</span>.
dans <span className="text-gray-200 font-medium">{tableLabel[confirmState.table]}</span>.
Cette action est irréversible.
</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => setConfirmState(null)}
className="px-4 py-2 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 transition-colors"
>
Annuler
</button>
<button
onClick={confirmPurge}
disabled={purgeLoading}
className="px-4 py-2 rounded-lg text-xs bg-red-600 hover:bg-red-700 disabled:opacity-50 transition-colors text-white font-medium"
>
{purgeLoading ? 'Suppression…' : 'Supprimer'}
</button>
</div>
</div>
</div>
</>
}
confirmLabel={purgeLoading ? 'Suppression…' : 'Supprimer'}
loading={purgeLoading}
onConfirm={confirmPurge}
onCancel={() => setConfirmState(null)}
/>
)}
</div>