From bd0beddbf49263973418239940499424c71f0930 Mon Sep 17 00:00:00 2001 From: PauI Ostrovckij Date: Sat, 1 Nov 2025 22:17:55 +0300 Subject: [PATCH 1/4] [feature] Responsive design for Admin Panel; Fix bug 77162 --- AdminPanel/client/src/App.css | 27 +++++++++ AdminPanel/client/src/App.js | 9 ++- .../src/components/AccessRules/AccessRules.js | 2 +- .../AccessRules/AccessRules.module.scss | 52 +++++++++++++++++ .../FixedSaveButton.module.scss | 24 ++++++++ .../FixedSaveButtonGroup.js | 2 +- .../client/src/components/Input/Input.js | 2 +- .../src/components/Input/Input.module.scss | 4 +- AdminPanel/client/src/components/Menu/Menu.js | 18 +++++- .../src/components/Menu/Menu.module.scss | 25 +++++++++ .../components/MobileHeader/MobileHeader.js | 16 ++++++ .../MobileHeader/MobileHeader.module.scss | 56 +++++++++++++++++++ .../components/PasswordInput/PasswordInput.js | 2 +- .../PasswordInput/PasswordInput.module.scss | 8 ++- .../src/components/ScrollToTop/ScrollToTop.js | 17 ++++++ .../client/src/components/Section/Section.js | 17 ++++++ .../components/Section/Section.module.scss | 38 +++++++++++++ .../src/components/Tabs/Tabs.module.scss | 16 ++++++ .../src/pages/AiIntegration/css/plugins.css | 1 + .../pages/ChangePassword/ChangePassword.js | 5 +- .../client/src/pages/Expiration/Expiration.js | 9 +-- .../pages/Expiration/Expiration.module.scss | 6 ++ .../client/src/pages/FileLimits/FileLimits.js | 17 +++--- .../src/pages/Forgotten/Forgotten.module.scss | 34 ----------- .../src/pages/HealthCheck/HealthCheck.js | 5 +- .../src/pages/LoggerConfig/LoggerConfig.js | 5 +- .../NotitifcationConfig/NotificationConfig.js | 33 +++++------ .../RequestFiltering/RequestFiltering.js | 8 +-- .../client/src/pages/Settings/Settings.js | 20 +++---- .../client/src/pages/Settings/Settings.scss | 28 ---------- .../src/pages/Statistics/styles.module.css | 7 +++ .../src/pages/WOPISettings/WOPISettings.js | 22 ++++---- 32 files changed, 393 insertions(+), 142 deletions(-) create mode 100644 AdminPanel/client/src/components/MobileHeader/MobileHeader.js create mode 100644 AdminPanel/client/src/components/MobileHeader/MobileHeader.module.scss create mode 100644 AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js create mode 100644 AdminPanel/client/src/components/Section/Section.js create mode 100644 AdminPanel/client/src/components/Section/Section.module.scss diff --git a/AdminPanel/client/src/App.css b/AdminPanel/client/src/App.css index 8b4c671f..c206680d 100644 --- a/AdminPanel/client/src/App.css +++ b/AdminPanel/client/src/App.css @@ -84,3 +84,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; + } +} diff --git a/AdminPanel/client/src/App.js b/AdminPanel/client/src/App.js index 468d984a..99702661 100644 --- a/AdminPanel/client/src/App.js +++ b/AdminPanel/client/src/App.js @@ -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 (
+ setIsMobileMenuOpen(prev => !prev)} />
- + setIsMobileMenuOpen(false)} /> + {isMobileMenuOpen ?
setIsMobileMenuOpen(false)} aria-hidden='true'>
: null}
+ } /> diff --git a/AdminPanel/client/src/components/AccessRules/AccessRules.js b/AdminPanel/client/src/components/AccessRules/AccessRules.js index 817de96f..0bee5731 100644 --- a/AdminPanel/client/src/components/AccessRules/AccessRules.js +++ b/AdminPanel/client/src/components/AccessRules/AccessRules.js @@ -45,7 +45,7 @@ function AccessRules({rules = [], onChange}) { value={newRule.value} onChange={value => setNewRule({...newRule, value})} onKeyPress={handleKeyPress} - width='calc(100% - 32px)' + width='100%' />
+
DocServer Admin Panel
+
+ ); +} + +export default MobileHeader; diff --git a/AdminPanel/client/src/components/MobileHeader/MobileHeader.module.scss b/AdminPanel/client/src/components/MobileHeader/MobileHeader.module.scss new file mode 100644 index 00000000..ff73e7a4 --- /dev/null +++ b/AdminPanel/client/src/components/MobileHeader/MobileHeader.module.scss @@ -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; + } +} diff --git a/AdminPanel/client/src/components/PasswordInput/PasswordInput.js b/AdminPanel/client/src/components/PasswordInput/PasswordInput.js index a038d12e..ebad0243 100644 --- a/AdminPanel/client/src/components/PasswordInput/PasswordInput.js +++ b/AdminPanel/client/src/components/PasswordInput/PasswordInput.js @@ -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); diff --git a/AdminPanel/client/src/components/PasswordInput/PasswordInput.module.scss b/AdminPanel/client/src/components/PasswordInput/PasswordInput.module.scss index 623b004b..f381179b 100644 --- a/AdminPanel/client/src/components/PasswordInput/PasswordInput.module.scss +++ b/AdminPanel/client/src/components/PasswordInput/PasswordInput.module.scss @@ -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; diff --git a/AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js b/AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js new file mode 100644 index 00000000..25fe13f9 --- /dev/null +++ b/AdminPanel/client/src/components/ScrollToTop/ScrollToTop.js @@ -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; +} diff --git a/AdminPanel/client/src/components/Section/Section.js b/AdminPanel/client/src/components/Section/Section.js new file mode 100644 index 00000000..76e83d8b --- /dev/null +++ b/AdminPanel/client/src/components/Section/Section.js @@ -0,0 +1,17 @@ +import styles from './Section.module.scss'; + +function Section({title, description, children, className = ''}) { + return ( +
+ {(title || description) && ( +
+ {title ?
{title}
: null} + {description ?
{description}
: null} +
+ )} +
{children}
+
+ ); +} + +export default Section; diff --git a/AdminPanel/client/src/components/Section/Section.module.scss b/AdminPanel/client/src/components/Section/Section.module.scss new file mode 100644 index 00000000..b8ab8097 --- /dev/null +++ b/AdminPanel/client/src/components/Section/Section.module.scss @@ -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; + } +} diff --git a/AdminPanel/client/src/components/Tabs/Tabs.module.scss b/AdminPanel/client/src/components/Tabs/Tabs.module.scss index 550e05c6..13e793ea 100644 --- a/AdminPanel/client/src/components/Tabs/Tabs.module.scss +++ b/AdminPanel/client/src/components/Tabs/Tabs.module.scss @@ -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; diff --git a/AdminPanel/client/src/pages/AiIntegration/css/plugins.css b/AdminPanel/client/src/pages/AiIntegration/css/plugins.css index 23750ffc..bc99ee6d 100644 --- a/AdminPanel/client/src/pages/AiIntegration/css/plugins.css +++ b/AdminPanel/client/src/pages/AiIntegration/css/plugins.css @@ -350,6 +350,7 @@ a.aboutlink:active { user-select: none; -moz-user-select: none; -webkit-user-select: none; + padding: 0; } .select2-dropdown, diff --git a/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js b/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js index 8b4d0cfa..25151e1b 100644 --- a/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js +++ b/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js @@ -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() { Update your admin panel password
-
+
{passwordSuccess &&
Password changed successfully!
} {passwordError &&
{passwordError}
} @@ -98,7 +99,7 @@ function ChangePassword() {
-
+
); diff --git a/AdminPanel/client/src/pages/Expiration/Expiration.js b/AdminPanel/client/src/pages/Expiration/Expiration.js index ec54e9be..5f9f521f 100644 --- a/AdminPanel/client/src/pages/Expiration/Expiration.js +++ b/AdminPanel/client/src/pages/Expiration/Expiration.js @@ -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 ( -
+
-
+ ); case 'session-management': return ( -
+
-
+ ); default: return null; diff --git a/AdminPanel/client/src/pages/Expiration/Expiration.module.scss b/AdminPanel/client/src/pages/Expiration/Expiration.module.scss index 1c8d8042..d07b7064 100644 --- a/AdminPanel/client/src/pages/Expiration/Expiration.module.scss +++ b/AdminPanel/client/src/pages/Expiration/Expiration.module.scss @@ -47,3 +47,9 @@ .pageWithFixedSave { padding-bottom: 40px; } + +@media (max-width: 767px) { + .pageWithFixedSave { + padding-bottom: 0; + } +} diff --git a/AdminPanel/client/src/pages/FileLimits/FileLimits.js b/AdminPanel/client/src/pages/FileLimits/FileLimits.js index 7c4fb525..6b9c7d86 100644 --- a/AdminPanel/client/src/pages/FileLimits/FileLimits.js +++ b/AdminPanel/client/src/pages/FileLimits/FileLimits.js @@ -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() { File Size Limits Configure maximum file sizes and download limits for document processing -
-
Download Limits
- +
-
- -
-
Input File Size Limits
-
Configure uncompressed size limits for different document types when processing ZIP archives
+ +
-
+ Save Changes diff --git a/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss b/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss index 69649d32..504dcfab 100644 --- a/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss +++ b/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss @@ -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; - } - } - } - } -} diff --git a/AdminPanel/client/src/pages/HealthCheck/HealthCheck.js b/AdminPanel/client/src/pages/HealthCheck/HealthCheck.js index 755886be..6fdc9088 100644 --- a/AdminPanel/client/src/pages/HealthCheck/HealthCheck.js +++ b/AdminPanel/client/src/pages/HealthCheck/HealthCheck.js @@ -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() { Health Check Monitor the status of DocService backend -
+

DocService Status

@@ -58,7 +59,7 @@ function HealthCheck() {
)}
-
+ {loading ? 'Checking...' : 'Refresh'} diff --git a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js index 164a7ea6..45140bfd 100644 --- a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js +++ b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js @@ -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() { Logger Configuration Configure the logging level for the application -
+
@@ -118,7 +119,7 @@ function LoggerConfig() { {getFieldError(CONFIG_PATHS.logLevel) &&
{getFieldError(CONFIG_PATHS.logLevel)}
}
-
+ Save Changes diff --git a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js index f0769953..57f692ee 100644 --- a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js +++ b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js @@ -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 ( -
+
-
+ ); case 'defaults': return ( -
+
-
+ ); case 'notifications': return ( <> -
-
License Expiration Warning
-
Configure email notifications when the license is about to expire
+
-
+ -
-
License Expiration Error
-
Configure email notifications when the license has expired
+
-
+ -
-
License Limit Edit
-
Configure email notifications when the edit limit is reached
+
-
+ -
-
License Limit Live Viewer
-
Configure email notifications when the live viewer limit is reached
+
-
+ ); default: diff --git a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js index 3d573e2e..8d8f5799 100644 --- a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js +++ b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js @@ -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. -
-

IP Address Filtering

-

Control access based on IP address types to enhance security.

- +
-
+ Save Changes diff --git a/AdminPanel/client/src/pages/Settings/Settings.js b/AdminPanel/client/src/pages/Settings/Settings.js index d3b5404b..e80470a8 100644 --- a/AdminPanel/client/src/pages/Settings/Settings.js +++ b/AdminPanel/client/src/pages/Settings/Settings.js @@ -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 = () => {

Settings

-
-
-
-
-

Reset Configuration

-

This will reset all configuration settings to their default values. This action cannot be undone.

-
-
- -
-
-
+
+
+ +
); diff --git a/AdminPanel/client/src/pages/Settings/Settings.scss b/AdminPanel/client/src/pages/Settings/Settings.scss index 3257373e..4cdd81b9 100644 --- a/AdminPanel/client/src/pages/Settings/Settings.scss +++ b/AdminPanel/client/src/pages/Settings/Settings.scss @@ -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%; - } - } - } - } - } - } -} diff --git a/AdminPanel/client/src/pages/Statistics/styles.module.css b/AdminPanel/client/src/pages/Statistics/styles.module.css index 49e6994d..65276efb 100644 --- a/AdminPanel/client/src/pages/Statistics/styles.module.css +++ b/AdminPanel/client/src/pages/Statistics/styles.module.css @@ -4,6 +4,13 @@ gap: 24px; } +@media (max-width: 767px) { + .topRow { + flex-direction: column; + gap: 12px; + } +} + .modeBar { margin: 8px 0 16px; font-size: 14px; diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js index cc4b7ac4..d10e2b97 100644 --- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js +++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js @@ -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,15 +124,13 @@ function WOPISettings() { WOPI Settings Configure WOPI (Web Application Open Platform Interface) support for document editing -
+
-
+ {localWopiEnabled && ( <> -
-
Lock Settings
-
Configure document lock refresh interval for WOPI sessions.
+
-
+ -
-
Key Management
-
- Rotate WOPI encryption keys. Current keys will be moved to "Old" and new keys will be generated. -
+
Do not rotate keys more than once per 24 hours; storage may not refresh in time and authentication can fail.
@@ -171,7 +169,7 @@ function WOPISettings() { description="Generate new encryption keys. Current keys will be moved to 'Old'." />
- + )} From bde82221dd768a6407f6167a704ddb367324dbed Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Tue, 11 Nov 2025 19:38:35 +0300 Subject: [PATCH 2/4] [bug] Add link to password recovery documentation; For bug 77637 --- AdminPanel/client/src/App.css | 10 ++++++++++ AdminPanel/client/src/pages/Login/LoginPage.js | 14 +++++++++++++- .../client/src/pages/Login/styles.module.css | 9 ++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/AdminPanel/client/src/App.css b/AdminPanel/client/src/App.css index 8b4c671f..dc1e44d7 100644 --- a/AdminPanel/client/src/App.css +++ b/AdminPanel/client/src/App.css @@ -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 { diff --git a/AdminPanel/client/src/pages/Login/LoginPage.js b/AdminPanel/client/src/pages/Login/LoginPage.js index e4641643..db63d366 100644 --- a/AdminPanel/client/src/pages/Login/LoginPage.js +++ b/AdminPanel/client/src/pages/Login/LoginPage.js @@ -39,7 +39,19 @@ export default function Login() {

ONLYOFFICE Admin Panel

Enter your password to access the admin panel

-

The session is valid for 60 minutes.

+
+

The session is valid for 60 minutes.

+

+ Need to reset your password? See{' '} + + password recovery documentation + +

+
diff --git a/AdminPanel/client/src/pages/Login/styles.module.css b/AdminPanel/client/src/pages/Login/styles.module.css index d6c74a09..7702c57a 100644 --- a/AdminPanel/client/src/pages/Login/styles.module.css +++ b/AdminPanel/client/src/pages/Login/styles.module.css @@ -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; } From 2eec4ebafd7c5a48e064ab18d86ff7e4e0d32613 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Wed, 12 Nov 2025 18:55:25 +0300 Subject: [PATCH 3/4] [feature] Add close button to mobile menu; remove input max-width in mobile --- .../src/components/Input/Input.module.scss | 6 ++ AdminPanel/client/src/components/Menu/Menu.js | 7 ++- .../src/components/Menu/Menu.module.scss | 59 ++++++++++++++++++- .../components/MobileHeader/MobileHeader.js | 2 +- .../src/pages/WOPISettings/WOPISettings.js | 2 - .../WOPISettings/WOPISettings.module.scss | 10 ++-- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/AdminPanel/client/src/components/Input/Input.module.scss b/AdminPanel/client/src/components/Input/Input.module.scss index 00fb6488..5182b206 100644 --- a/AdminPanel/client/src/components/Input/Input.module.scss +++ b/AdminPanel/client/src/components/Input/Input.module.scss @@ -63,3 +63,9 @@ color: #dc3545; margin-top: 4px; } + +@media (max-width: 767px) { + .input { + max-width: 100%; + } +} diff --git a/AdminPanel/client/src/components/Menu/Menu.js b/AdminPanel/client/src/components/Menu/Menu.js index 7bec8add..b51e1f5e 100644 --- a/AdminPanel/client/src/components/Menu/Menu.js +++ b/AdminPanel/client/src/components/Menu/Menu.js @@ -38,15 +38,16 @@ function Menu({isOpen, onClose}) { return (
-
+
@@ -156,7 +155,6 @@ function WOPISettings() { value={maskKey(wopiPublicKey)} disabled placeholder='No key generated' - width='400px' style={{fontFamily: 'Courier New, monospace'}} />
diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss index 8a5634d2..3b7e49b3 100644 --- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss +++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss @@ -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 From 8fb8d22ffbfc3ed491296e8691a7f164061ec603 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Wed, 12 Nov 2025 20:08:39 +0300 Subject: [PATCH 4/4] [bug] Add internal/cluster/pre-stop handler to graceful shutdown in k8s cluster --- DocService/sources/DocsCoServer.js | 35 ++++++++++++++++++++++++++--- DocService/sources/canvasservice.js | 8 ++++++- DocService/sources/server.js | 3 +++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index af9e9bf2..6a9b83d9 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -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 diff --git a/DocService/sources/canvasservice.js b/DocService/sources/canvasservice.js index 826080fd..6baff245 100644 --- a/DocService/sources/canvasservice.js +++ b/DocService/sources/canvasservice.js @@ -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 diff --git a/DocService/sources/server.js b/DocService/sources/server.js index ef826174..a04d7bed 100644 --- a/DocService/sources/server.js +++ b/DocService/sources/server.js @@ -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) {