Add server control buttons to dashboard with confirmation dialogs
All checks were successful
Deploy GSM / deploy (push) Successful in 24s

- Add ConfirmModal component for stop/restart confirmations
- Add start/stop/restart buttons to ServerCard (moderator/admin only)
- Add confirmation dialogs to ServerDetail for stop/restart actions
- Add btn-sm CSS class for smaller buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 13:40:45 +01:00
parent 3dc7e9e7e7
commit a07e8df3e7
5 changed files with 151 additions and 7 deletions

View File

@@ -0,0 +1,23 @@
export default function ConfirmModal({ title, message, confirmText, cancelText, onConfirm, onCancel, variant = 'danger' }) {
const confirmBtnClass = variant === 'danger' ? 'btn btn-destructive' : 'btn btn-primary'
return (
<div className="modal-backdrop fade-in" onClick={onCancel}>
<div className="modal fade-in-scale max-w-md" onClick={(e) => e.stopPropagation()}>
<div className="p-6">
<h2 className="text-lg font-semibold text-white mb-2">{title}</h2>
<p className="text-neutral-400 mb-6">{message}</p>
<div className="flex gap-3 justify-end">
<button onClick={onCancel} className="btn btn-ghost">
{cancelText || 'Abbrechen'}
</button>
<button onClick={onConfirm} className={confirmBtnClass}>
{confirmText || 'Bestätigen'}
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,3 +1,7 @@
import { useState } from 'react'
import { serverAction } from '../api'
import ConfirmModal from './ConfirmModal'
const serverInfo = {
minecraft: {
address: 'minecraft.zeasy.dev',
@@ -64,8 +68,10 @@ const getServerInfo = (serverName) => {
return null
}
export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings }) {
export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings, isModerator, token, onServerAction }) {
const defaultInfo = getServerInfo(server.name)
const [confirmAction, setConfirmAction] = useState(null)
const [actionLoading, setActionLoading] = useState(false)
// Merge default info with database display settings (database takes priority)
const info = defaultInfo ? {
@@ -119,6 +125,30 @@ export default function ServerCard({ server, onClick, isAuthenticated, isGuest,
const isClickable = !isUnreachable && !isGuest && onClick
const handleAction = async (action) => {
setActionLoading(true)
try {
await serverAction(token, server.id, action)
if (onServerAction) onServerAction()
} catch (err) {
console.error(err)
} finally {
setActionLoading(false)
setConfirmAction(null)
}
}
const handleActionClick = (action, e) => {
e.stopPropagation()
if (action === 'start') {
handleAction(action)
} else {
setConfirmAction(action)
}
}
const isActionDisabled = actionLoading || server.status === 'starting' || server.status === 'stopping'
return (
<div
className={isUnreachable ? "card p-5 opacity-50 cursor-not-allowed" : (isClickable ? "card card-clickable p-5" : "card p-5")}
@@ -240,6 +270,55 @@ export default function ServerCard({ server, onClick, isAuthenticated, isGuest,
</div>
</div>
)}
{/* Server Controls - only for moderators */}
{isModerator && !isUnreachable && (
<div className="mt-3 pt-3 border-t border-neutral-800">
<div className="flex flex-wrap gap-2">
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
<>
<button
onClick={(e) => handleActionClick('stop', e)}
disabled={isActionDisabled}
className="btn btn-destructive btn-sm"
>
{server.status === 'stopping' ? 'Stoppt...' : 'Stoppen'}
</button>
<button
onClick={(e) => handleActionClick('restart', e)}
disabled={isActionDisabled}
className="btn btn-secondary btn-sm"
>
{server.status === 'starting' ? 'Startet...' : 'Neustarten'}
</button>
</>
) : (
<button
onClick={(e) => handleActionClick('start', e)}
disabled={isActionDisabled}
className="btn btn-primary btn-sm"
>
{server.status === 'starting' ? 'Startet...' : 'Starten'}
</button>
)}
</div>
</div>
)}
{/* Confirmation Modal */}
{confirmAction && (
<ConfirmModal
title={confirmAction === 'stop' ? 'Server stoppen?' : 'Server neustarten?'}
message={confirmAction === 'stop'
? `Bist du sicher, dass du ${server.name} stoppen möchtest?`
: `Bist du sicher, dass du ${server.name} neustarten möchtest?`
}
confirmText={confirmAction === 'stop' ? 'Stoppen' : 'Neustarten'}
variant={confirmAction === 'stop' ? 'danger' : 'primary'}
onConfirm={() => handleAction(confirmAction)}
onCancel={() => setConfirmAction(null)}
/>
)}
</div>
)
}

View File

@@ -52,6 +52,11 @@ button {
cursor: not-allowed;
}
.btn-sm {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
.btn-primary {
background-color: #fafafa;
color: #0a0a0a;

View File

@@ -9,7 +9,7 @@ import LoginModal from '../components/LoginModal'
export default function Dashboard({ onLogout }) {
const navigate = useNavigate()
const { user, token, loading: userLoading, isSuperadmin, role } = useUser()
const { user, token, loading: userLoading, isSuperadmin, isModerator, role } = useUser()
const [servers, setServers] = useState([])
const [displaySettings, setDisplaySettings] = useState({})
const [loading, setLoading] = useState(true)
@@ -259,6 +259,9 @@ export default function Dashboard({ onLogout }) {
isAuthenticated={isAuthenticated}
isGuest={isGuest}
displaySettings={displaySettings[server.id]}
isModerator={isModerator}
token={token}
onServerAction={fetchServers}
/>
</div>
))}

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import { getServers, serverAction, sendRcon, getServerLogs, getWhitelist, getFactorioCurrentSave, getAutoShutdownSettings, setAutoShutdownSettings, getDisplaySettings, setDisplaySettings } from '../api'
import { useUser } from '../context/UserContext'
import MetricsChart from '../components/MetricsChart'
import ConfirmModal from '../components/ConfirmModal'
import FactorioWorldManager from '../components/FactorioWorldManager'
import PalworldConfigEditor from '../components/PalworldConfigEditor'
import ZomboidConfigEditor from '../components/ZomboidConfigEditor'
@@ -42,6 +43,9 @@ export default function ServerDetail() {
// Auto-shutdown state
const [autoShutdown, setAutoShutdown] = useState({ enabled: false, timeoutMinutes: 15, emptySinceMinutes: null })
// Confirmation modal state
const [confirmAction, setConfirmAction] = useState(null)
const [autoShutdownLoading, setAutoShutdownLoading] = useState(false)
// Display settings state (superadmin only)
@@ -139,6 +143,21 @@ export default function ServerDetail() {
}
}
const handleActionWithConfirm = (action) => {
if (action === 'start') {
handleAction(action)
} else {
setConfirmAction(action)
}
}
const handleConfirmAction = () => {
if (confirmAction) {
handleAction(confirmAction)
setConfirmAction(null)
}
}
const handleRcon = async (e) => {
e.preventDefault()
if (!rconCommand.trim()) return
@@ -443,14 +462,14 @@ const formatUptime = (seconds) => {
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
<>
<button
onClick={() => handleAction('stop')}
onClick={() => handleActionWithConfirm('stop')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-destructive"
>
{server.status === 'stopping' ? 'Stoppt...' : 'Server stoppen'}
</button>
<button
onClick={() => handleAction('restart')}
onClick={() => handleActionWithConfirm('restart')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-secondary"
>
@@ -459,7 +478,7 @@ const formatUptime = (seconds) => {
</>
) : (
<button
onClick={() => handleAction('start')}
onClick={() => handleActionWithConfirm('start')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-primary"
>
@@ -776,6 +795,21 @@ const formatUptime = (seconds) => {
</div>
)}
</main>
{/* Confirmation Modal */}
{confirmAction && (
<ConfirmModal
title={confirmAction === 'stop' ? 'Server stoppen?' : 'Server neustarten?'}
message={confirmAction === 'stop'
? `Bist du sicher, dass du ${server.name} stoppen möchtest?`
: `Bist du sicher, dass du ${server.name} neustarten möchtest?`
}
confirmText={confirmAction === 'stop' ? 'Stoppen' : 'Neustarten'}
variant={confirmAction === 'stop' ? 'danger' : 'primary'}
onConfirm={handleConfirmAction}
onCancel={() => setConfirmAction(null)}
/>
)}
</div>
)
}