[feature] Add shutdown in Admin Panel

This commit is contained in:
PauI Ostrovckij
2025-12-16 14:57:30 +03:00
parent d2c54fd6dc
commit c3072def6f
6 changed files with 199 additions and 3 deletions

View File

@ -193,6 +193,39 @@ export const checkHealth = async () => {
return true;
};
export const getMaintenanceStatus = async () => {
const response = await safeFetch(`${DOCSERVICE_URL}/internal/cluster/inactive`, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to get maintenance status');
}
return response.json();
};
export const enterMaintenanceMode = async () => {
const response = await safeFetch(`${DOCSERVICE_URL}/internal/cluster/inactive`, {
method: 'PUT',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to enter maintenance mode');
}
return response.json();
};
export const exitMaintenanceMode = async () => {
const response = await safeFetch(`${DOCSERVICE_URL}/internal/cluster/inactive`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to exit maintenance mode');
}
return response.json();
};
export const resetConfiguration = async (paths = ['*']) => {
const pathsArray = Array.isArray(paths) ? paths : [paths];

View File

@ -0,0 +1,92 @@
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';
import Button from '../Button/Button';
import Section from '../Section/Section';
import {enterMaintenanceMode, exitMaintenanceMode, getMaintenanceStatus} from '../../api';
import styles from './ShutdownTab.module.scss';
const SHUTDOWN_TITLE = 'Server Shutdown';
const SHUTDOWN_DESCRIPTION =
'Control server shutdown mode. In shutdown mode, new editor connections are blocked and existing sessions are gracefully closed. This does not restart the server process.';
const ShutdownTab = () => {
const queryClient = useQueryClient();
const {data: maintenanceStatus = {shutdown: false}, isLoading: statusLoading} = useQuery({
queryKey: ['maintenanceStatus'],
queryFn: getMaintenanceStatus,
retry: false,
onError: () => {}
});
const shutdownMutation = useMutation({
mutationFn: enterMaintenanceMode,
onSuccess: () => {
queryClient.setQueryData(['maintenanceStatus'], prev => ({...(prev || {}), shutdown: true}));
}
});
const resumeMutation = useMutation({
mutationFn: exitMaintenanceMode,
onSuccess: () => {
queryClient.setQueryData(['maintenanceStatus'], prev => ({...(prev || {}), shutdown: false}));
}
});
const handleShutdown = async () => {
const confirmed = window.confirm(
'Shutdown server? This will block new editor connections and gracefully close existing sessions. The server will remain in this state until you resume it or restart it manually.'
);
if (!confirmed) {
return;
}
await shutdownMutation.mutateAsync();
};
const handleResume = async () => {
await resumeMutation.mutateAsync();
};
if (statusLoading) {
return (
<Section title={SHUTDOWN_TITLE} description={SHUTDOWN_DESCRIPTION}>
<div className={styles.loading}>Loading status...</div>
</Section>
);
}
return (
<Section title={SHUTDOWN_TITLE} description={SHUTDOWN_DESCRIPTION}>
<div className={styles.container}>
<div className={`${styles.statusBadge} ${maintenanceStatus.shutdown ? styles.statusBadgeWarning : styles.statusBadgeSuccess}`}>
<div className={styles.statusTitle}>Current Status:</div>
<div className={styles.statusContent}>
{maintenanceStatus.shutdown ? (
<span className={styles.statusTextWarning}> Shutdown Mode Active - New connections blocked</span>
) : (
<span className={styles.statusTextSuccess}> Normal Operation - Accepting new connections</span>
)}
</div>
</div>
<div>
<Button onClick={handleShutdown} disabled={maintenanceStatus.shutdown || shutdownMutation.isPending} disableResult={true}>
Shutdown
</Button>
<p className={styles.buttonDescription}>
Blocks new editor connections and closes existing sessions. Use before restarting nodes or performing maintenance.
</p>
</div>
<div>
<Button onClick={handleResume} disabled={!maintenanceStatus.shutdown || resumeMutation.isPending}>
Resume
</Button>
<p className={styles.buttonDescription}>Returns server to normal mode and allows new editor connections.</p>
</div>
</div>
</Section>
);
};
export default ShutdownTab;

View File

@ -0,0 +1,50 @@
.container {
display: flex;
flex-direction: column;
gap: 16px;
}
.loading {
padding: 16px;
text-align: center;
color: #666;
}
.statusBadge {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 8px;
}
.statusBadgeSuccess {
background-color: #d4edda;
border: 1px solid #28a745;
}
.statusBadgeWarning {
background-color: #fff3cd;
border: 1px solid #ffc107;
}
.statusTitle {
font-weight: 600;
margin-bottom: 4px;
}
.statusContent {
font-size: 14px;
}
.statusTextSuccess {
color: #155724;
}
.statusTextWarning {
color: #856404;
}
.buttonDescription {
margin-top: 8px;
font-size: 13px;
color: #666;
}

View File

@ -4,11 +4,12 @@ import Button from '../../components/Button/Button';
import Section from '../../components/Section/Section';
import ConfigViewer from '../../components/ConfigViewer/ConfigViewer';
import Tabs from '../../components/Tabs/Tabs';
import ShutdownTab from '../../components/ShutdownTab/ShutdownTab';
import './Settings.scss';
const settingsTabs = [
{key: 'configuration', label: 'Configuration'},
{key: 'server-reload', label: 'Server Reload'}
{key: 'shutdown', label: 'Shutdown'}
];
const Settings = () => {
@ -43,8 +44,8 @@ const Settings = () => {
</Section>
</>
);
case 'server-reload':
return null;
case 'shutdown':
return <ShutdownTab />;
default:
return null;
}

View File

@ -4785,6 +4785,25 @@ function getConnections() {
}
exports.getConnections = getConnections;
/**
* Get shutdown status
* @param {Object} req - Express request
* @param {Object} res - Express response
*/
exports.getShutdownStatus = function (req, res) {
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
res.setHeader('Content-Type', 'application/json');
res.json({
shutdown: getIsShutdown()
});
} catch (err) {
ctx.logger.error('getShutdownStatus error %s', err.stack);
res.status(500).json({error: 'Internal server error'});
}
};
exports.getEditorConnectionsCount = function (req, res) {
const ctx = new operationContext.Context();
let count = 0;

View File

@ -274,6 +274,7 @@ docsCoServer.install(server, app, () => {
app.use('/info', infoRouter(docsCoServer.getConnections));
app.put('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.delete('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.get('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.getShutdownStatus);
app.put('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);
app.delete('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);