Merge pull request 'fix/admin-panel' (#65) from fix/admin-panel into release/v9.1.0

Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/65
This commit is contained in:
Sergey Konovalov
2025-09-29 23:28:46 +00:00
12 changed files with 50 additions and 40 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -1,18 +1,17 @@
import {useSelector, useDispatch} from 'react-redux'; import {useDispatch} from 'react-redux';
import {useLocation, useNavigate} from 'react-router-dom'; import {useLocation, useNavigate} from 'react-router-dom';
import {selectIsAuthenticated} from '../../store/slices/userSlice';
import {clearConfig} from '../../store/slices/configSlice'; import {clearConfig} from '../../store/slices/configSlice';
import {logout} from '../../api'; import {logout} from '../../api';
import MenuItem from './MenuItem/MenuItem'; import MenuItem from './MenuItem/MenuItem';
import AppMenuLogo from '../../assets/AppMenuLogo.svg'; import AppMenuLogo from '../../assets/AppMenuLogo.svg';
import {menuItems} from '../../config/menuItems'; import {menuItems} from '../../config/menuItems';
import styles from './Menu.module.scss'; import styles from './Menu.module.scss';
import FileIcon from '../../assets/File.svg';
function Menu() { function Menu() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const isAuthenticated = useSelector(selectIsAuthenticated);
const handleLogout = async () => { const handleLogout = async () => {
try { try {
@ -49,17 +48,16 @@ function Menu() {
<div className={styles['menu__menuItems']}> <div className={styles['menu__menuItems']}>
{menuItems.map(item => ( {menuItems.map(item => (
<MenuItem key={item.key} label={item.label} isActive={isActiveItem(item.path)} onClick={() => handleMenuItemClick(item)} /> <MenuItem
key={item.key}
label={item.label}
isActive={isActiveItem(item.path)}
onClick={() => handleMenuItemClick(item)}
icon={FileIcon}
/>
))} ))}
<MenuItem label='Logout' isActive={false} onClick={handleLogout} />
</div> </div>
{isAuthenticated && (
<div className={styles['menu__logoutContainer']}>
<button onClick={handleLogout} className={styles['menu__logoutButton']}>
Logout
</button>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -60,7 +60,7 @@
} }
&__logoutContainer { &__logoutContainer {
margin-top: auto; margin-left: 48px;
} }
&__logoutButton { &__logoutButton {

View File

@ -1,10 +1,9 @@
import FileIcon from '../../../assets/File.svg';
import styles from './MenuItem.module.scss'; import styles from './MenuItem.module.scss';
function MenuItem({label, isActive, onClick}) { function MenuItem({label, isActive, onClick, icon}) {
return ( return (
<div className={`${styles.menuItem} ${isActive ? styles['menuItem--active'] : ''}`} onClick={onClick}> <div className={`${styles.menuItem} ${isActive ? styles['menuItem--active'] : ''}`} onClick={onClick}>
<img src={FileIcon} alt='' className={styles['menuItem__icon']} /> {icon ? <img src={icon} alt='' className={styles['menuItem__icon']} /> : <div className={styles['menuItem__icon']} />}
<span className={styles['menuItem__label']}>{label}</span> <span className={styles['menuItem__label']}>{label}</span>
</div> </div>
); );

View File

@ -20,7 +20,7 @@
} }
&--disabled { &--disabled {
background: #ff865c; background: #ffd4c5;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -137,34 +137,34 @@ function Expiration() {
<div className={styles.tabPanel}> <div className={styles.tabPanel}>
<div className={styles.formRow}> <div className={styles.formRow}>
<Input <Input
label='Files Cron Expression' label='Cache Cleanup Cron Expression'
value={localSettings.filesCron} value={localSettings.filesCron}
onChange={value => handleFieldChange('filesCron', value)} onChange={value => handleFieldChange('filesCron', value)}
placeholder='0 0 */2 * * *' placeholder='0 0 */2 * * *'
description='Cron expression for file cleanup schedule (6 fields: second minute hour day month day_of_week)' description='Cron expression for cleaning up expired cached files and temporary data (6 fields: second minute hour day month day_of_week)'
error={getFieldError(CONFIG_PATHS.filesCron)} error={getFieldError(CONFIG_PATHS.filesCron)}
/> />
</div> </div>
<div className={styles.formRow}> <div className={styles.formRow}>
<Input <Input
label='Documents Cron Expression' label='Auto-Save & Presence Cleanup Cron Expression'
value={localSettings.documentsCron} value={localSettings.documentsCron}
onChange={value => handleFieldChange('documentsCron', value)} onChange={value => handleFieldChange('documentsCron', value)}
placeholder='0 0 */2 * * *' placeholder='0 0 */2 * * *'
description='Cron expression for document cleanup schedule (6 fields: second minute hour day month day_of_week)' description='Cron expression for auto-saving documents with pending changes and cleaning up expired user presence data (6 fields: second minute hour day month day_of_week)'
error={getFieldError(CONFIG_PATHS.documentsCron)} error={getFieldError(CONFIG_PATHS.documentsCron)}
/> />
</div> </div>
<div className={styles.formRow}> <div className={styles.formRow}>
<Input <Input
label='Files Expiration Time (seconds)' label='Cache File Retention Time (seconds)'
type='number' type='number'
value={localSettings.files} value={localSettings.files}
onChange={value => handleFieldChange('files', value)} onChange={value => handleFieldChange('files', value)}
placeholder='3600' placeholder='3600'
description='Time in seconds after which files expire and can be cleaned up' description='How long to keep cached files before marking them as expired and eligible for cleanup (default: 86400 = 24 hours)'
min='0' min='0'
error={getFieldError(CONFIG_PATHS.files)} error={getFieldError(CONFIG_PATHS.files)}
/> />

View File

@ -46,6 +46,7 @@ export default function Login() {
onChange={setTenantName} onChange={setTenantName}
placeholder='Enter your tenant name' placeholder='Enter your tenant name'
description='The name of your tenant organization' description='The name of your tenant organization'
error={error}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</div> </div>

View File

@ -107,3 +107,6 @@ app.use((err, req, res, _next) => {
server.listen(port, () => { server.listen(port, () => {
operationContext.global.logger.warn('AdminPanel server listening on port %d', port); operationContext.global.logger.warn('AdminPanel server listening on port %d', port);
}); });
//after all required modules in all files
moduleReloader.finalizeConfigWithRuntime();

View File

@ -50,6 +50,10 @@ function reloadNpmModule(moduleName) {
} }
} }
// Backup original NODE_CONFIG to avoid growing environment
const prevNodeConfig = process.env.NODE_CONFIG;
let nodeConfigOverridden = false;
/** /**
* Requires config module with runtime configuration support. * Requires config module with runtime configuration support.
* Temporarily sets NODE_CONFIG for reload, then restores environment to prevent E2BIG. * Temporarily sets NODE_CONFIG for reload, then restores environment to prevent E2BIG.
@ -59,10 +63,6 @@ function reloadNpmModule(moduleName) {
function requireConfigWithRuntime(opt_additionalConfig) { function requireConfigWithRuntime(opt_additionalConfig) {
let config = require('config'); let config = require('config');
// Backup original NODE_CONFIG to avoid growing environment
const prevNodeConfig = process.env.NODE_CONFIG;
let nodeConfigOverridden = false;
try { try {
const configFilePath = config.get('runtimeConfig.filePath'); const configFilePath = config.get('runtimeConfig.filePath');
if (configFilePath) { if (configFilePath) {
@ -88,20 +88,23 @@ function requireConfigWithRuntime(opt_additionalConfig) {
if (err.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
console.error('Failed to load runtime config: %s', err.stack); console.error('Failed to load runtime config: %s', err.stack);
} }
} finally {
// Restore original NODE_CONFIG to keep env small and avoid E2BIG on Windows/pkg
if (nodeConfigOverridden) {
if (typeof prevNodeConfig === 'undefined') {
delete process.env.NODE_CONFIG;
} else {
process.env.NODE_CONFIG = prevNodeConfig;
}
}
} }
return config; return config;
} }
function finalizeConfigWithRuntime() {
// Restore original NODE_CONFIG to keep env small and avoid E2BIG on Windows/pkg
if (nodeConfigOverridden) {
if (typeof prevNodeConfig === 'undefined') {
delete process.env.NODE_CONFIG;
} else {
process.env.NODE_CONFIG = prevNodeConfig;
}
}
}
module.exports = { module.exports = {
reloadNpmModule, reloadNpmModule,
requireConfigWithRuntime requireConfigWithRuntime,
finalizeConfigWithRuntime
}; };

View File

@ -509,3 +509,6 @@ process.on('uncaughtException', err => {
process.exit(1); process.exit(1);
}); });
}); });
//after all required modules in all files
moduleReloader.finalizeConfigWithRuntime();

View File

@ -66,7 +66,7 @@ function initializeSharp() {
} }
}); });
} }
if (sharp) { if (sharp) {
// todo test. // todo test.
// Set concurrency to 2 for better performance // Set concurrency to 2 for better performance

View File

@ -33,10 +33,10 @@
'use strict'; 'use strict';
const cluster = require('cluster'); const cluster = require('cluster');
const logger = require('./../../Common/sources/logger');
const operationContext = require('./../../Common/sources/operationContext');
const moduleReloader = require('./../../Common/sources/moduleReloader'); const moduleReloader = require('./../../Common/sources/moduleReloader');
const config = moduleReloader.requireConfigWithRuntime(); const config = moduleReloader.requireConfigWithRuntime();
const logger = require('./../../Common/sources/logger');
const operationContext = require('./../../Common/sources/operationContext');
if (cluster.isMaster) { if (cluster.isMaster) {
const fs = require('fs'); const fs = require('fs');
@ -104,3 +104,6 @@ process.on('uncaughtException', err => {
process.exit(1); process.exit(1);
}); });
}); });
//after all required modules in all files
moduleReloader.finalizeConfigWithRuntime();