mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[feature] Add shutdown in Admin Panel
This commit is contained in:
@ -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];
|
||||
|
||||
|
||||
92
AdminPanel/client/src/components/ShutdownTab/ShutdownTab.js
Normal file
92
AdminPanel/client/src/components/ShutdownTab/ShutdownTab.js
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user