mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
Merge pull request 'fix/bug-77637' (#90) from fix/bug-77637 into release/v9.2.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/90
This commit is contained in:
@ -75,6 +75,16 @@ body::-webkit-scrollbar-thumb {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff6f3d;
|
||||
text-decoration: underline;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #e55a2b;
|
||||
}
|
||||
|
||||
/* Spinner animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
@ -84,3 +94,30 @@ body::-webkit-scrollbar-thumb {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 767px) {
|
||||
.mainContent {
|
||||
padding: 21px;
|
||||
padding-top: 72px; /* 56px header + 16px content padding */
|
||||
/* Hide scrollbar UI on mobile while preserving scroll */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.mainContent::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none; /* Chrome, Safari */
|
||||
}
|
||||
|
||||
.mobileMenuBackdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
z-index: 1050;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {Provider} from 'react-redux';
|
||||
import {useState} from 'react';
|
||||
import {Routes, Route, Navigate, BrowserRouter} from 'react-router-dom';
|
||||
import './App.css';
|
||||
import {store} from './store';
|
||||
@ -6,18 +7,24 @@ import AuthWrapper from './components/AuthWrapper/AuthWrapper';
|
||||
import ConfigLoader from './components/ConfigLoader/ConfigLoader';
|
||||
import {useSchemaLoader} from './hooks/useSchemaLoader';
|
||||
import Menu from './components/Menu/Menu';
|
||||
import MobileHeader from './components/MobileHeader/MobileHeader';
|
||||
import ScrollToTop from './components/ScrollToTop/ScrollToTop';
|
||||
import {menuItems} from './config/menuItems';
|
||||
import {getBasename} from './utils/paths';
|
||||
|
||||
function AppContent() {
|
||||
useSchemaLoader();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className='app'>
|
||||
<AuthWrapper>
|
||||
<MobileHeader isOpen={isMobileMenuOpen} onMenuToggle={() => setIsMobileMenuOpen(prev => !prev)} />
|
||||
<div className='appLayout'>
|
||||
<Menu />
|
||||
<Menu isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} />
|
||||
{isMobileMenuOpen ? <div className='mobileMenuBackdrop' onClick={() => setIsMobileMenuOpen(false)} aria-hidden='true'></div> : null}
|
||||
<div className='mainContent'>
|
||||
<ScrollToTop />
|
||||
<ConfigLoader>
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to='/statistics' replace />} />
|
||||
|
||||
@ -45,7 +45,7 @@ function AccessRules({rules = [], onChange}) {
|
||||
value={newRule.value}
|
||||
onChange={value => setNewRule({...newRule, value})}
|
||||
onKeyPress={handleKeyPress}
|
||||
width='calc(100% - 32px)'
|
||||
width='100%'
|
||||
/>
|
||||
</div>
|
||||
<button className={styles.addButton} onClick={handleAddRule} disabled={!newRule.value.trim()}>
|
||||
|
||||
@ -154,3 +154,55 @@
|
||||
font-style: italic;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.addRule {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.addRule :global(.selectWrapper) {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.addRule :global(.select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rulesList {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
height: auto;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.ruleValue {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ruleType {
|
||||
order: 2;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,15 @@
|
||||
padding: 16px 0;
|
||||
width: calc(100% - 256px);
|
||||
// background-color: #fafafa;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
position: static;
|
||||
left: auto;
|
||||
right: auto;
|
||||
width: auto;
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.saveButtonWrapper {
|
||||
@ -18,9 +27,24 @@
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
// Add bottom padding to pages to prevent content from being hidden behind fixed button
|
||||
.pageWithFixedSave {
|
||||
padding-bottom: 40px; // Adjust based on button height + padding
|
||||
}
|
||||
|
||||
/* Removed separate modifier; mobile behavior is now global via media query above */
|
||||
|
||||
.fixedSaveGroup {
|
||||
@media (max-width: 767px) {
|
||||
padding: 32px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ function FixedSaveButtonGroup({buttons = []}) {
|
||||
if (!buttons || buttons.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.fixedSaveContainer}>
|
||||
<div className={`${styles.fixedSaveContainer} ${styles.fixedSaveGroup}`}>
|
||||
<div
|
||||
className={styles.saveButtonWrapper}
|
||||
style={{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styles from './Input.module.scss';
|
||||
|
||||
function Input({label, value, onChange, type = 'text', placeholder = '', error = null, description = null, width, ...props}) {
|
||||
const inputStyle = width ? {width} : {};
|
||||
const inputStyle = width ? {maxWidth: width} : {};
|
||||
|
||||
return (
|
||||
<div className={styles.inputGroup}>
|
||||
|
||||
@ -18,8 +18,10 @@
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 294px;
|
||||
width: 100%;
|
||||
max-width: 294px;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
@ -61,3 +63,9 @@
|
||||
color: #dc3545;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import {menuItems} from '../../config/menuItems';
|
||||
import styles from './Menu.module.scss';
|
||||
import FileIcon from '../../assets/File.svg';
|
||||
|
||||
function Menu() {
|
||||
function Menu({isOpen, onClose}) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
@ -27,6 +27,9 @@ function Menu() {
|
||||
// Clear config to force reload when switching pages
|
||||
dispatch(clearConfig());
|
||||
navigate(item.path);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const isActiveItem = path => {
|
||||
@ -34,16 +37,17 @@ function Menu() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.menu}>
|
||||
<div className={styles['menu__content']}>
|
||||
<div className={`${styles.menu} ${isOpen ? styles['menu--open'] : ''}`}>
|
||||
<button className={styles['menu__closeButton']} onClick={onClose} aria-label='Close menu' />
|
||||
<div className={styles['menu__header']}>
|
||||
<div className={styles['menu__logoContainer']}>
|
||||
<img src={AppMenuLogo} alt='ONLYOFFICE' className={styles['menu__logo']} />
|
||||
</div>
|
||||
|
||||
<div className={styles['menu__title']}>DocServer Admin Panel</div>
|
||||
|
||||
<div className={styles['menu__separator']}></div>
|
||||
</div>
|
||||
|
||||
<div className={styles['menu__content']}>
|
||||
<div className={styles['menu__menuItems']}>
|
||||
{menuItems.map(item => (
|
||||
<MenuItem
|
||||
@ -54,7 +58,16 @@ function Menu() {
|
||||
icon={FileIcon}
|
||||
/>
|
||||
))}
|
||||
<MenuItem label='Logout' isActive={false} onClick={handleLogout} />
|
||||
<MenuItem
|
||||
label='Logout'
|
||||
isActive={false}
|
||||
onClick={async () => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
await handleLogout();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
.menu {
|
||||
position: relative;
|
||||
width: 256px;
|
||||
height: 100vh;
|
||||
background: #f9f9f9;
|
||||
@ -6,12 +7,47 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
/* Mobile off-canvas behavior */
|
||||
@media (max-width: 767px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1101; /* above backdrop, below header z-index 1100? keep slightly above */
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&--open {
|
||||
@media (max-width: 767px) {
|
||||
width: 240px;
|
||||
transform: translateX(0);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04),
|
||||
0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
.menu__header {
|
||||
padding-left: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.menu__content {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
flex-shrink: 0;
|
||||
padding-left: 32px;
|
||||
padding-top: 32px;
|
||||
padding-right: 16px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
padding-left: 32px;
|
||||
padding-top: 32px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@ -88,4 +124,46 @@
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
&__closeButton {
|
||||
display: none;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: #333333;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:hover::after {
|
||||
background: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import styles from './MobileHeader.module.scss';
|
||||
|
||||
function MobileHeader({onMenuToggle, isOpen}) {
|
||||
return (
|
||||
<div className={`${styles.mobileHeader} ${isOpen ? styles['mobileHeader--open'] : ''}`}>
|
||||
<button className={styles.burger} onClick={onMenuToggle} aria-label='Menu' aria-expanded={isOpen}>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div className={styles.title}>DocServer Admin Panel</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileHeader;
|
||||
@ -0,0 +1,56 @@
|
||||
.mobileHeader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.mobileHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 16px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mobileHeader--open {
|
||||
background: #ececec;
|
||||
}
|
||||
|
||||
.burger {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.burger span {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
background: #333333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 133%;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import styles from './PasswordInput.module.scss';
|
||||
|
||||
function PasswordInput({label, value, onChange, placeholder = '', error = null, description = null, width, isValid = true, ...props}) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const inputStyle = width ? {width} : {};
|
||||
const inputStyle = width ? {maxWidth: width} : {};
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword(!showPassword);
|
||||
|
||||
@ -19,12 +19,14 @@
|
||||
|
||||
.inputContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: inline-block; /* match input width so eye stays inside */
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 270px;
|
||||
padding: 12px 40px 12px 16px; /* Keep original padding */
|
||||
width: 100%;
|
||||
max-width: 270px;
|
||||
padding: 12px 45px 12px 16px; /* Keep original padding */
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
17
AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js
Normal file
17
AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js
Normal file
@ -0,0 +1,17 @@
|
||||
import {useEffect} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
|
||||
export default function ScrollToTop() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const scroller = document.querySelector('.mainContent');
|
||||
if (scroller && typeof scroller.scrollTo === 'function') {
|
||||
scroller.scrollTo({top: 0, left: 0, behavior: 'auto'});
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
17
AdminPanel/client/src/components/Section/Section.js
Normal file
17
AdminPanel/client/src/components/Section/Section.js
Normal file
@ -0,0 +1,17 @@
|
||||
import styles from './Section.module.scss';
|
||||
|
||||
function Section({title, description, children, className = ''}) {
|
||||
return (
|
||||
<div className={`${styles.section} ${className}`}>
|
||||
{(title || description) && (
|
||||
<div className={styles.header}>
|
||||
{title ? <div className={styles.title}>{title}</div> : null}
|
||||
{description ? <div className={styles.description}>{description}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Section;
|
||||
38
AdminPanel/client/src/components/Section/Section.module.scss
Normal file
38
AdminPanel/client/src/components/Section/Section.module.scss
Normal file
@ -0,0 +1,38 @@
|
||||
.section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px 32px 0 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin-top: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.header {
|
||||
padding: 12px 16px 0 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
@ -24,13 +24,29 @@
|
||||
color: #666666;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
color: inherit; /* keep current color; active tab will override below */
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: #ff6f3d;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: #ff6f3d; /* ensure orange while focused/pressed */
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@ -350,6 +350,7 @@ a.aboutlink:active {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-dropdown,
|
||||
|
||||
@ -6,6 +6,7 @@ import PasswordInput from '../../components/PasswordInput/PasswordInput';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import PasswordInputWithRequirements from '../../components/PasswordInputWithRequirements/PasswordInputWithRequirements';
|
||||
import {usePasswordValidation} from '../../utils/passwordValidation';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './ChangePassword.module.scss';
|
||||
|
||||
function ChangePassword() {
|
||||
@ -56,7 +57,7 @@ function ChangePassword() {
|
||||
<PageDescription>Update your admin panel password</PageDescription>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.section}>
|
||||
<Section>
|
||||
{passwordSuccess && <div className={styles.successMessage}>Password changed successfully!</div>}
|
||||
|
||||
{passwordError && <div className={styles.errorMessage}>{passwordError}</div>}
|
||||
@ -98,7 +99,7 @@ function ChangePassword() {
|
||||
|
||||
<FixedSaveButton onClick={handlePasswordChange} disabled={!canSubmit()} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import PageDescription from '../../components/PageDescription/PageDescription';
|
||||
import Tabs from '../../components/Tabs/Tabs';
|
||||
import Input from '../../components/Input/Input';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './Expiration.module.scss';
|
||||
|
||||
const expirationTabs = [
|
||||
@ -134,7 +135,7 @@ function Expiration() {
|
||||
switch (activeTab) {
|
||||
case 'garbage-collection':
|
||||
return (
|
||||
<div className={styles.tabPanel}>
|
||||
<Section>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Cache Cleanup Cron Expression'
|
||||
@ -182,11 +183,11 @@ function Expiration() {
|
||||
error={getFieldError(CONFIG_PATHS.filesremovedatonce)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
case 'session-management':
|
||||
return (
|
||||
<div className={styles.tabPanel}>
|
||||
<Section>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Session Idle Timeout'
|
||||
@ -208,7 +209,7 @@ function Expiration() {
|
||||
error={getFieldError(CONFIG_PATHS.sessionabsolute)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
|
||||
@ -47,3 +47,9 @@
|
||||
.pageWithFixedSave {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.pageWithFixedSave {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader';
|
||||
import PageDescription from '../../components/PageDescription/PageDescription';
|
||||
import Input from '../../components/Input/Input';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './FileLimits.module.scss';
|
||||
|
||||
function FileLimits() {
|
||||
@ -168,9 +169,7 @@ function FileLimits() {
|
||||
<PageHeader>File Size Limits</PageHeader>
|
||||
<PageDescription>Configure maximum file sizes and download limits for document processing</PageDescription>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>Download Limits</div>
|
||||
|
||||
<Section title='Download Limits'>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Max Download Bytes'
|
||||
@ -183,12 +182,12 @@ function FileLimits() {
|
||||
error={getFieldError(CONFIG_PATHS.maxDownloadBytes)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>Input File Size Limits</div>
|
||||
<div className={styles.sectionDescription}>Configure uncompressed size limits for different document types when processing ZIP archives</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title='Input File Size Limits'
|
||||
description='Configure uncompressed size limits for different document types when processing ZIP archives'
|
||||
>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Word Documents (DOCX, DOTX, DOCM, DOTM)'
|
||||
@ -228,7 +227,7 @@ function FileLimits() {
|
||||
description='Maximum uncompressed size for Visio document archives'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}>
|
||||
Save Changes
|
||||
|
||||
@ -132,37 +132,3 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.forgottenPage {
|
||||
// padding: 15px;
|
||||
|
||||
.pageHeader {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.filesList {
|
||||
.fileRow {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.fileName {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import {checkHealth} from '../../api';
|
||||
import PageHeader from '../../components/PageHeader/PageHeader';
|
||||
import PageDescription from '../../components/PageDescription/PageDescription';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './HealthCheck.module.scss';
|
||||
|
||||
function HealthCheck() {
|
||||
@ -39,7 +40,7 @@ function HealthCheck() {
|
||||
<PageHeader>Health Check</PageHeader>
|
||||
<PageDescription>Monitor the status of DocService backend</PageDescription>
|
||||
|
||||
<div className={styles.statusCard}>
|
||||
<Section>
|
||||
<div className={styles.statusHeader}>
|
||||
<div className={styles.statusIndicator} style={{backgroundColor: getStatusColor()}} />
|
||||
<h3 className={styles.statusTitle}>DocService Status</h3>
|
||||
@ -58,7 +59,7 @@ function HealthCheck() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<FixedSaveButton onClick={fetchHealthStatus} disabled={loading} disableResult={true}>
|
||||
{loading ? 'Checking...' : 'Refresh'}
|
||||
|
||||
@ -8,6 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader';
|
||||
import PageDescription from '../../components/PageDescription/PageDescription';
|
||||
import Select from '../../components/Select/Select';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './LoggerConfig.module.scss';
|
||||
|
||||
const LOG_LEVELS = [
|
||||
@ -104,7 +105,7 @@ function LoggerConfig() {
|
||||
<PageHeader>Logger Configuration</PageHeader>
|
||||
<PageDescription>Configure the logging level for the application</PageDescription>
|
||||
|
||||
<div className={styles.configSection}>
|
||||
<Section title='Logger Settings'>
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.fieldGroup}>
|
||||
<label className={styles.label}>Log Level:</label>
|
||||
@ -118,7 +119,7 @@ function LoggerConfig() {
|
||||
{getFieldError(CONFIG_PATHS.logLevel) && <div className={styles.error}>{getFieldError(CONFIG_PATHS.logLevel)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}>
|
||||
Save Changes
|
||||
|
||||
@ -39,7 +39,19 @@ export default function Login() {
|
||||
<div className={styles.loginCard}>
|
||||
<h1 className={styles.title}>ONLYOFFICE Admin Panel</h1>
|
||||
<p className={styles.subtitle}>Enter your password to access the admin panel</p>
|
||||
<p className={styles.description}>The session is valid for 60 minutes.</p>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<p className={styles.description}>The session is valid for 60 minutes.</p>
|
||||
<p className={styles.description}>
|
||||
Need to reset your password? See{' '}
|
||||
<a
|
||||
href='https://helpcenter.onlyoffice.com/docs/installation/docs-admin-panel.aspx#passwordresetrecovery_block'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
password recovery documentation
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.inputGroup}>
|
||||
|
||||
@ -31,10 +31,17 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin: 0 0 32px 0;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import Tabs from '../../components/Tabs/Tabs';
|
||||
import Input from '../../components/Input/Input';
|
||||
import Checkbox from '../../components/Checkbox/Checkbox';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './NotificationConfig.module.scss';
|
||||
|
||||
const emailConfigTabs = [
|
||||
@ -159,7 +160,7 @@ function EmailConfig() {
|
||||
switch (activeTab) {
|
||||
case 'smtp-server':
|
||||
return (
|
||||
<div className={styles.tabPanel}>
|
||||
<Section>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='SMTP Host:'
|
||||
@ -207,11 +208,11 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.smtpPassword)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
case 'defaults':
|
||||
return (
|
||||
<div className={styles.tabPanel}>
|
||||
<Section>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Default From Email:'
|
||||
@ -235,14 +236,12 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.defaultToEmail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
case 'notifications':
|
||||
return (
|
||||
<>
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>License Expiration Warning</div>
|
||||
<div className={styles.sectionDescription}>Configure email notifications when the license is about to expire</div>
|
||||
<Section title='License Expiration Warning' description='Configure email notifications when the license is about to expire'>
|
||||
<div className={styles.formRow}>
|
||||
<Checkbox
|
||||
label='Enable'
|
||||
@ -261,11 +260,9 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.licenseExpirationWarningRepeatInterval)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>License Expiration Error</div>
|
||||
<div className={styles.sectionDescription}>Configure email notifications when the license has expired</div>
|
||||
<Section title='License Expiration Error' description='Configure email notifications when the license has expired'>
|
||||
<div className={styles.formRow}>
|
||||
<Checkbox
|
||||
label='Enable'
|
||||
@ -284,11 +281,9 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.licenseExpirationErrorRepeatInterval)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>License Limit Edit</div>
|
||||
<div className={styles.sectionDescription}>Configure email notifications when the edit limit is reached</div>
|
||||
<Section title='License Limit Edit' description='Configure email notifications when the edit limit is reached'>
|
||||
<div className={styles.formRow}>
|
||||
<Checkbox
|
||||
label='Enable'
|
||||
@ -307,11 +302,9 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.licenseLimitEditRepeatInterval)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>License Limit Live Viewer</div>
|
||||
<div className={styles.sectionDescription}>Configure email notifications when the live viewer limit is reached</div>
|
||||
<Section title='License Limit Live Viewer' description='Configure email notifications when the live viewer limit is reached'>
|
||||
<div className={styles.formRow}>
|
||||
<Checkbox
|
||||
label='Enable'
|
||||
@ -330,7 +323,7 @@ function EmailConfig() {
|
||||
error={getFieldError(CONFIG_PATHS.licenseLimitLiveViewerRepeatInterval)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
|
||||
@ -8,6 +8,7 @@ import Checkbox from '../../components/Checkbox/Checkbox';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import PageHeader from '../../components/PageHeader/PageHeader';
|
||||
import PageDescription from '../../components/PageDescription/PageDescription';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './RequestFiltering.module.scss';
|
||||
|
||||
function RequestFiltering() {
|
||||
@ -91,10 +92,7 @@ function RequestFiltering() {
|
||||
Configure request filtering settings to control which IP addresses are allowed to make requests to the server.
|
||||
</PageDescription>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>IP Address Filtering</h2>
|
||||
<p className={styles.sectionDescription}>Control access based on IP address types to enhance security.</p>
|
||||
|
||||
<Section title='IP Address Filtering' description='Control access based on IP address types to enhance security.'>
|
||||
<div className={styles.formRow}>
|
||||
<Checkbox
|
||||
label='Use IP filtering for requests'
|
||||
@ -124,7 +122,7 @@ function RequestFiltering() {
|
||||
error={getFieldError(CONFIG_PATHS.allowMetaIPAddress)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}>
|
||||
Save Changes
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {resetConfiguration} from '../../api';
|
||||
import Button from '../../components/Button/Button';
|
||||
import Section from '../../components/Section/Section';
|
||||
import './Settings.scss';
|
||||
|
||||
const Settings = () => {
|
||||
@ -17,18 +18,13 @@ const Settings = () => {
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
|
||||
<div className='settings-content'>
|
||||
<div className='settings-section'>
|
||||
<div className='settings-item'>
|
||||
<div className='settings-info'>
|
||||
<h3>Reset Configuration</h3>
|
||||
<p>This will reset all configuration settings to their default values. This action cannot be undone.</p>
|
||||
</div>
|
||||
<div className='settings-actions'>
|
||||
<Button onClick={handleResetConfig}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='settings-content' title='Settings'>
|
||||
<Section
|
||||
title='Reset Configuration'
|
||||
description='This will reset all configuration settings to their default values. This action cannot be undone.'
|
||||
>
|
||||
<Button onClick={handleResetConfig}>Reset</Button>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -118,31 +118,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.settings-page {
|
||||
padding: 15px;
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
.settings-section {
|
||||
.settings-item {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.settings-actions {
|
||||
width: 100%;
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,13 @@
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.topRow {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.modeBar {
|
||||
margin: 8px 0 16px;
|
||||
font-size: 14px;
|
||||
|
||||
@ -12,6 +12,7 @@ import Input from '../../components/Input/Input';
|
||||
import Checkbox from '../../components/Checkbox/Checkbox';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Note from '../../components/Note/Note';
|
||||
import Section from '../../components/Section/Section';
|
||||
import styles from './WOPISettings.module.scss';
|
||||
|
||||
function WOPISettings() {
|
||||
@ -123,32 +124,28 @@ function WOPISettings() {
|
||||
<PageHeader>WOPI Settings</PageHeader>
|
||||
<PageDescription>Configure WOPI (Web Application Open Platform Interface) support for document editing</PageDescription>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<Section>
|
||||
<ToggleSwitch label='WOPI' checked={localWopiEnabled} onChange={handleWopiEnabledChange} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{localWopiEnabled && (
|
||||
<>
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>Lock Settings</div>
|
||||
<div className={styles.sectionDescription}>Configure document lock refresh interval for WOPI sessions.</div>
|
||||
<Section title='Lock Settings' description='Configure document lock refresh interval for WOPI sessions.'>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Refresh Lock Interval'
|
||||
value={localRefreshLockInterval}
|
||||
onChange={handleRefreshLockIntervalChange}
|
||||
placeholder='10m'
|
||||
width='200px'
|
||||
description="Time interval for refreshing document locks (e.g., '10m', '1h', '30s')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<div className={styles.sectionTitle}>Key Management</div>
|
||||
<div className={styles.sectionDescription}>
|
||||
Rotate WOPI encryption keys. Current keys will be moved to "Old" and new keys will be generated.
|
||||
</div>
|
||||
<Section
|
||||
title='Key Management'
|
||||
description='Rotate WOPI encryption keys. Current keys will be moved to "Old" and new keys will be generated.'
|
||||
>
|
||||
<div className={styles.noteWrapper}>
|
||||
<Note type='warning'>Do not rotate keys more than once per 24 hours; storage may not refresh in time and authentication can fail.</Note>
|
||||
</div>
|
||||
@ -158,7 +155,6 @@ function WOPISettings() {
|
||||
value={maskKey(wopiPublicKey)}
|
||||
disabled
|
||||
placeholder='No key generated'
|
||||
width='400px'
|
||||
style={{fontFamily: 'Courier New, monospace'}}
|
||||
/>
|
||||
</div>
|
||||
@ -171,7 +167,7 @@ function WOPISettings() {
|
||||
description="Generate new encryption keys. Current keys will be moved to 'Old'."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -42,11 +42,11 @@
|
||||
}
|
||||
|
||||
.formRow {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure SaveButton aligns properly in form row
|
||||
|
||||
@ -175,6 +175,7 @@ const lockDocumentsTimerId = {}; //to drop connection that can't unlockDocument
|
||||
let pubsub;
|
||||
let queue;
|
||||
let shutdownFlag = false;
|
||||
let preStopFlag = false;
|
||||
const expDocumentsStep = gc.getCronStep(cfgExpDocumentsCron);
|
||||
|
||||
const MIN_SAVE_EXPIRATION = 60000;
|
||||
@ -191,6 +192,10 @@ function getIsShutdown() {
|
||||
return shutdownFlag;
|
||||
}
|
||||
|
||||
function getIsPreStop() {
|
||||
return preStopFlag;
|
||||
}
|
||||
|
||||
function getEditorConfig(ctx) {
|
||||
let tenEditor = ctx.getCfg('services.CoAuthoring.editor', cfgEditor);
|
||||
tenEditor = JSON.parse(JSON.stringify(tenEditor));
|
||||
@ -1534,7 +1539,7 @@ function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) {
|
||||
|
||||
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
|
||||
yield editorData.cleanDocumentOnExit(ctx, docId);
|
||||
if (editorStatProxy?.deleteKey) {
|
||||
if (preStopFlag && editorStatProxy?.deleteKey) {
|
||||
yield editorStatProxy.deleteKey(docId);
|
||||
}
|
||||
//remove changes
|
||||
@ -1772,6 +1777,7 @@ exports.hasEditors = hasEditors;
|
||||
exports.getEditorsCountPromise = co.wrap(getEditorsCount);
|
||||
exports.getCallback = getCallback;
|
||||
exports.getIsShutdown = getIsShutdown;
|
||||
exports.getIsPreStop = getIsPreStop;
|
||||
exports.hasChanges = hasChanges;
|
||||
exports.cleanDocumentOnExitPromise = co.wrap(cleanDocumentOnExit);
|
||||
exports.cleanDocumentOnExitNoChangesPromise = co.wrap(cleanDocumentOnExitNoChanges);
|
||||
@ -2178,7 +2184,7 @@ exports.install = function (server, app, callbackFunction) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (hvals?.length <= 0 && editorStatProxy?.deleteKey) {
|
||||
if (preStopFlag && hvals?.length <= 0 && editorStatProxy?.deleteKey) {
|
||||
yield editorStatProxy.deleteKey(docId);
|
||||
}
|
||||
}
|
||||
@ -4214,9 +4220,12 @@ exports.install = function (server, app, callbackFunction) {
|
||||
ctx.initFromConnection(conn);
|
||||
//todo group by tenant
|
||||
yield ctx.initTenantCache();
|
||||
const tenExpSessionIdle = ms(ctx.getCfg('services.CoAuthoring.expire.sessionidle', cfgExpSessionIdle));
|
||||
let tenExpSessionIdle = ms(ctx.getCfg('services.CoAuthoring.expire.sessionidle', cfgExpSessionIdle)) || 0;
|
||||
const tenExpSessionAbsolute = ms(ctx.getCfg('services.CoAuthoring.expire.sessionabsolute', cfgExpSessionAbsolute));
|
||||
const tenExpSessionCloseCommand = ms(ctx.getCfg('services.CoAuthoring.expire.sessionclosecommand', cfgExpSessionCloseCommand));
|
||||
if (preStopFlag && (tenExpSessionIdle > 5 * 60 * 1000 || tenExpSessionIdle <= 0)) {
|
||||
tenExpSessionIdle = 5 * 60 * 1000; //5 minutes
|
||||
}
|
||||
|
||||
const maxMs = nowMs + Math.max(tenExpSessionCloseCommand, expDocumentsStep);
|
||||
let tenant = tenants[ctx.tenant];
|
||||
@ -4732,6 +4741,26 @@ exports.shutdown = function (req, res) {
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.preStop = async function (req, res) {
|
||||
let output = false;
|
||||
const ctx = new operationContext.Context();
|
||||
try {
|
||||
ctx.initFromRequest(req);
|
||||
await ctx.initTenantCache();
|
||||
preStopFlag = req.method === 'PUT';
|
||||
ctx.logger.info('preStop set flag', preStopFlag);
|
||||
if (preStopFlag) {
|
||||
await gc.checkFileExpire(0);
|
||||
}
|
||||
output = true;
|
||||
} catch (err) {
|
||||
ctx.logger.error('preStop error %s', err.stack);
|
||||
} finally {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.send(output.toString());
|
||||
ctx.logger.info('preStop end');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Get active connections array
|
||||
* @returns {Array} Active connections
|
||||
|
||||
@ -467,6 +467,9 @@ const cleanupCache = co.wrap(function* (ctx, docId) {
|
||||
const removeRes = yield taskResult.remove(ctx, docId);
|
||||
if (removeRes.affectedRows > 0) {
|
||||
yield storage.deletePath(ctx, docId);
|
||||
if (docsCoServer?.editorStatProxy?.deleteKey) {
|
||||
yield docsCoServer.editorStatProxy.deleteKey(docId);
|
||||
}
|
||||
res = true;
|
||||
}
|
||||
ctx.logger.debug('cleanupCache docId=%s db.affectedRows=%d', docId, removeRes.affectedRows);
|
||||
@ -479,6 +482,9 @@ const cleanupCacheIf = co.wrap(function* (ctx, mask) {
|
||||
if (removeRes.affectedRows > 0) {
|
||||
sqlBase.deleteChanges(ctx, mask.key, null);
|
||||
yield storage.deletePath(ctx, mask.key);
|
||||
if (docsCoServer?.editorStatProxy?.deleteKey) {
|
||||
yield docsCoServer.editorStatProxy.deleteKey(mask.key);
|
||||
}
|
||||
res = true;
|
||||
}
|
||||
ctx.logger.debug('cleanupCacheIf db.affectedRows=%d', removeRes.affectedRows);
|
||||
@ -1300,7 +1306,7 @@ const commandSfcCallback = co.wrap(function* (ctx, cmd, isSfcm, isEncrypted) {
|
||||
//todo simultaneous opening
|
||||
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
|
||||
yield docsCoServer.editorData.cleanDocumentOnExit(ctx, docId);
|
||||
if (docsCoServer?.editorStatProxy?.deleteKey) {
|
||||
if (docsCoServer.getIsPreStop() && docsCoServer?.editorStatProxy?.deleteKey) {
|
||||
yield docsCoServer.editorStatProxy.deleteKey(docId);
|
||||
}
|
||||
//to unlock wopi file
|
||||
|
||||
@ -274,6 +274,9 @@ 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.put('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);
|
||||
app.delete('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);
|
||||
|
||||
app.get('/internal/connections/edit', docsCoServer.getEditorConnectionsCount);
|
||||
|
||||
function checkWopiEnable(req, res, next) {
|
||||
|
||||
Reference in New Issue
Block a user