Add server control buttons to dashboard with confirmation dialogs
All checks were successful
Deploy GSM / deploy (push) Successful in 24s
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:
23
gsm-frontend/src/components/ConfirmModal.jsx
Normal file
23
gsm-frontend/src/components/ConfirmModal.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { serverAction } from '../api'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
const serverInfo = {
|
const serverInfo = {
|
||||||
minecraft: {
|
minecraft: {
|
||||||
address: 'minecraft.zeasy.dev',
|
address: 'minecraft.zeasy.dev',
|
||||||
@@ -64,8 +68,10 @@ const getServerInfo = (serverName) => {
|
|||||||
return null
|
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 defaultInfo = getServerInfo(server.name)
|
||||||
|
const [confirmAction, setConfirmAction] = useState(null)
|
||||||
|
const [actionLoading, setActionLoading] = useState(false)
|
||||||
|
|
||||||
// Merge default info with database display settings (database takes priority)
|
// Merge default info with database display settings (database takes priority)
|
||||||
const info = defaultInfo ? {
|
const info = defaultInfo ? {
|
||||||
@@ -119,6 +125,30 @@ export default function ServerCard({ server, onClick, isAuthenticated, isGuest,
|
|||||||
|
|
||||||
const isClickable = !isUnreachable && !isGuest && onClick
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={isUnreachable ? "card p-5 opacity-50 cursor-not-allowed" : (isClickable ? "card card-clickable p-5" : "card p-5")}
|
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>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ button {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
color: #0a0a0a;
|
color: #0a0a0a;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import LoginModal from '../components/LoginModal'
|
|||||||
|
|
||||||
export default function Dashboard({ onLogout }) {
|
export default function Dashboard({ onLogout }) {
|
||||||
const navigate = useNavigate()
|
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 [servers, setServers] = useState([])
|
||||||
const [displaySettings, setDisplaySettings] = useState({})
|
const [displaySettings, setDisplaySettings] = useState({})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -259,6 +259,9 @@ export default function Dashboard({ onLogout }) {
|
|||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
isGuest={isGuest}
|
isGuest={isGuest}
|
||||||
displaySettings={displaySettings[server.id]}
|
displaySettings={displaySettings[server.id]}
|
||||||
|
isModerator={isModerator}
|
||||||
|
token={token}
|
||||||
|
onServerAction={fetchServers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 { getServers, serverAction, sendRcon, getServerLogs, getWhitelist, getFactorioCurrentSave, getAutoShutdownSettings, setAutoShutdownSettings, getDisplaySettings, setDisplaySettings } from '../api'
|
||||||
import { useUser } from '../context/UserContext'
|
import { useUser } from '../context/UserContext'
|
||||||
import MetricsChart from '../components/MetricsChart'
|
import MetricsChart from '../components/MetricsChart'
|
||||||
|
import ConfirmModal from '../components/ConfirmModal'
|
||||||
import FactorioWorldManager from '../components/FactorioWorldManager'
|
import FactorioWorldManager from '../components/FactorioWorldManager'
|
||||||
import PalworldConfigEditor from '../components/PalworldConfigEditor'
|
import PalworldConfigEditor from '../components/PalworldConfigEditor'
|
||||||
import ZomboidConfigEditor from '../components/ZomboidConfigEditor'
|
import ZomboidConfigEditor from '../components/ZomboidConfigEditor'
|
||||||
@@ -42,6 +43,9 @@ export default function ServerDetail() {
|
|||||||
|
|
||||||
// Auto-shutdown state
|
// Auto-shutdown state
|
||||||
const [autoShutdown, setAutoShutdown] = useState({ enabled: false, timeoutMinutes: 15, emptySinceMinutes: null })
|
const [autoShutdown, setAutoShutdown] = useState({ enabled: false, timeoutMinutes: 15, emptySinceMinutes: null })
|
||||||
|
|
||||||
|
// Confirmation modal state
|
||||||
|
const [confirmAction, setConfirmAction] = useState(null)
|
||||||
const [autoShutdownLoading, setAutoShutdownLoading] = useState(false)
|
const [autoShutdownLoading, setAutoShutdownLoading] = useState(false)
|
||||||
|
|
||||||
// Display settings state (superadmin only)
|
// Display settings state (superadmin only)
|
||||||
@@ -131,11 +135,26 @@ export default function ServerDetail() {
|
|||||||
await serverAction(token, server.id, action)
|
await serverAction(token, server.id, action)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetchServer()
|
fetchServer()
|
||||||
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActionWithConfirm = (action) => {
|
||||||
|
if (action === 'start') {
|
||||||
|
handleAction(action)
|
||||||
|
} else {
|
||||||
|
setConfirmAction(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmAction = () => {
|
||||||
|
if (confirmAction) {
|
||||||
|
handleAction(confirmAction)
|
||||||
|
setConfirmAction(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,14 +462,14 @@ const formatUptime = (seconds) => {
|
|||||||
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
|
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction('stop')}
|
onClick={() => handleActionWithConfirm('stop')}
|
||||||
disabled={server.status === 'stopping' || server.status === 'starting'}
|
disabled={server.status === 'stopping' || server.status === 'starting'}
|
||||||
className="btn btn-destructive"
|
className="btn btn-destructive"
|
||||||
>
|
>
|
||||||
{server.status === 'stopping' ? 'Stoppt...' : 'Server stoppen'}
|
{server.status === 'stopping' ? 'Stoppt...' : 'Server stoppen'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction('restart')}
|
onClick={() => handleActionWithConfirm('restart')}
|
||||||
disabled={server.status === 'stopping' || server.status === 'starting'}
|
disabled={server.status === 'stopping' || server.status === 'starting'}
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
>
|
>
|
||||||
@@ -459,7 +478,7 @@ const formatUptime = (seconds) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction('start')}
|
onClick={() => handleActionWithConfirm('start')}
|
||||||
disabled={server.status === 'stopping' || server.status === 'starting'}
|
disabled={server.status === 'stopping' || server.status === 'starting'}
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
>
|
>
|
||||||
@@ -776,6 +795,21 @@ const formatUptime = (seconds) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user