);
}
diff --git a/AdminPanel/client/src/pages/Expiration/Expiration.module.scss b/AdminPanel/client/src/pages/Expiration/Expiration.module.scss
index 693c8d1f..1c8d8042 100644
--- a/AdminPanel/client/src/pages/Expiration/Expiration.module.scss
+++ b/AdminPanel/client/src/pages/Expiration/Expiration.module.scss
@@ -43,3 +43,7 @@
display: flex;
justify-content: flex-start;
}
+
+.pageWithFixedSave {
+ padding-bottom: 40px;
+}
diff --git a/AdminPanel/client/src/pages/FileLimits/FileLimits.js b/AdminPanel/client/src/pages/FileLimits/FileLimits.js
index 3bd2b23e..7c4fb525 100644
--- a/AdminPanel/client/src/pages/FileLimits/FileLimits.js
+++ b/AdminPanel/client/src/pages/FileLimits/FileLimits.js
@@ -1,19 +1,18 @@
import {useState, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {fetchConfig, saveConfig, selectConfig, selectConfigLoading} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription';
import Input from '../../components/Input/Input';
-import SaveButton from '../../components/SaveButton/SaveButton';
+import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
import styles from './FileLimits.module.scss';
function FileLimits() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
- const loading = useSelector(selectConfigLoading);
const {validateField, getFieldError, hasValidationErrors} = useFieldValidation();
// Local state for form fields
@@ -37,13 +36,11 @@ function FileLimits() {
// Load config data when component mounts
useEffect(() => {
- if (!config) {
- dispatch(fetchConfig());
- } else {
+ if (config) {
const settings = {};
// Get max download bytes
- settings.maxDownloadBytes = getNestedValue(config, CONFIG_PATHS.maxDownloadBytes, '');
+ settings.maxDownloadBytes = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', '');
// Get input limits - need to handle array structure
const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []);
@@ -85,7 +82,7 @@ function FileLimits() {
let originalValue;
if (key === 'maxDownloadBytes') {
- originalValue = getNestedValue(config, CONFIG_PATHS.maxDownloadBytes, '');
+ originalValue = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', '');
} else {
// Handle input limits array structure for comparison
const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []);
@@ -166,12 +163,8 @@ function FileLimits() {
setHasChanges(false);
};
- if (loading) {
- return
Loading file limits settings...
;
- }
-
return (
-
+
File Size LimitsConfigure maximum file sizes and download limits for document processing
@@ -237,11 +230,9 @@ function FileLimits() {
-
-
- Save Changes
-
-
+
+ Save Changes
+
);
}
diff --git a/AdminPanel/client/src/pages/FileLimits/FileLimits.module.scss b/AdminPanel/client/src/pages/FileLimits/FileLimits.module.scss
index 35a97fb0..69380bd5 100644
--- a/AdminPanel/client/src/pages/FileLimits/FileLimits.module.scss
+++ b/AdminPanel/client/src/pages/FileLimits/FileLimits.module.scss
@@ -43,3 +43,7 @@
display: flex;
justify-content: flex-start;
}
+
+.pageWithFixedSave {
+ padding-bottom: 40px;
+}
diff --git a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js
index ff1e3f79..f9313aad 100644
--- a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js
+++ b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.js
@@ -1,18 +1,18 @@
-import {useState, useEffect} from 'react';
+import {useState, useRef} from 'react';
import {useDispatch, useSelector} from 'react-redux';
-import {fetchConfig, saveConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
import Checkbox from '../../components/Checkbox/Checkbox';
-import SaveButton from '../../components/SaveButton/SaveButton';
+import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription';
import styles from './RequestFiltering.module.scss';
function RequestFiltering() {
const dispatch = useDispatch();
- const {config, loading} = useSelector(state => state.config);
+ const config = useSelector(selectConfig);
const {validateField, getFieldError, hasValidationErrors} = useFieldValidation();
const [localSettings, setLocalSettings] = useState({
@@ -27,8 +27,8 @@ function RequestFiltering() {
allowMetaIPAddress: 'request-filtering-agent.allowMetaIPAddress'
};
- // Load initial values from config
- useEffect(() => {
+ const hasInitialized = useRef(false);
+ const resetToGlobalConfig = () => {
if (config) {
const newSettings = {};
Object.keys(CONFIG_PATHS).forEach(key => {
@@ -37,12 +37,12 @@ function RequestFiltering() {
});
setLocalSettings(newSettings);
}
- }, [config]);
-
- // Load config on component mount
- useEffect(() => {
- dispatch(fetchConfig());
- }, [dispatch]);
+ };
+ // Load initial values from config
+ if (config && !hasInitialized.current) {
+ resetToGlobalConfig();
+ hasInitialized.current = true;
+ }
// Handle field changes
const handleFieldChange = (field, value) => {
@@ -82,12 +82,8 @@ function RequestFiltering() {
setHasChanges(false);
};
- if (loading) {
- return
Loading request filtering settings...
;
- }
-
return (
-
+
Request Filtering
Configure request filtering settings to control which IP addresses are allowed to make requests to the server.
@@ -118,11 +114,9 @@ function RequestFiltering() {
-
-
- Save Changes
-
-
+
+ Save Changes
+
);
}
diff --git a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.module.scss b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.module.scss
index 3dad8d43..9ac325b9 100644
--- a/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.module.scss
+++ b/AdminPanel/client/src/pages/RequestFiltering/RequestFiltering.module.scss
@@ -67,3 +67,7 @@
display: flex;
justify-content: flex-start;
}
+
+.pageWithFixedSave {
+ padding-bottom: 40px;
+}
diff --git a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
index 2ebd7eac..0dcb893e 100644
--- a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
+++ b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
@@ -1,6 +1,6 @@
-import {useState, useEffect} from 'react';
+import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {fetchConfig, saveConfig, selectConfig, selectConfigLoading} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
@@ -8,7 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription';
import Tabs from '../../components/Tabs/Tabs';
import AccessRules from '../../components/AccessRules/AccessRules';
-import SaveButton from '../../components/SaveButton/SaveButton';
+import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
import styles from './SecuritySettings.module.scss';
const securityTabs = [{key: 'ip-filtering', label: 'IP Filtering'}];
@@ -16,30 +16,39 @@ const securityTabs = [{key: 'ip-filtering', label: 'IP Filtering'}];
function SecuritySettings() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
- const loading = useSelector(selectConfigLoading);
- const {validateField, getFieldError, hasValidationErrors} = useFieldValidation();
+ const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
const [activeTab, setActiveTab] = useState('ip-filtering');
const [localRules, setLocalRules] = useState([]);
const [hasChanges, setHasChanges] = useState(false);
- useEffect(() => {
- if (!config) {
- dispatch(fetchConfig());
- } else {
- // Get IP filtering rules from actual config
+ // Reset state and errors to global config
+ const resetToGlobalConfig = () => {
+ if (config) {
const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []);
-
- // Convert from backend format to UI format
const uiRules = ipFilterRules.map(rule => ({
type: rule.allowed ? 'Allow' : 'Deny',
value: rule.address
}));
-
setLocalRules(uiRules);
setHasChanges(false);
+ // Clear validation errors
+ clearFieldError('services.CoAuthoring.ipfilter.rules');
}
- }, [dispatch, config]);
+ };
+
+ // Handle tab change and reset state
+ const handleTabChange = newTab => {
+ setActiveTab(newTab);
+ resetToGlobalConfig();
+ };
+
+ const hasInitialized = useRef(false);
+
+ if (config && !hasInitialized.current) {
+ resetToGlobalConfig();
+ hasInitialized.current = true;
+ }
// Handle rules changes
const handleRulesChange = newRules => {
@@ -92,24 +101,18 @@ function SecuritySettings() {
}
};
- if (loading) {
- return
Loading security settings...
;
- }
-
return (
-
+
Security SettingsConfigure IP filtering, authentication, and security policies
-
+
{renderTabContent()}
-
-
- Save Changes
-
-
+
+ Save Changes
+
);
}
diff --git a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.module.scss b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.module.scss
index 83f55c5c..a1dffe09 100644
--- a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.module.scss
+++ b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.module.scss
@@ -2,6 +2,10 @@
padding: 0;
}
+.pageWithFixedSave {
+ padding-bottom: 40px;
+}
+
.loading {
display: flex;
justify-content: center;
diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
index 57a6a2f3..70fca1b4 100644
--- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
+++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
@@ -1,72 +1,124 @@
-import {useEffect, useState} from 'react';
+import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {fetchConfig, saveConfig, selectConfig, selectConfigLoading, selectSchema} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, rotateWopiKeysAction} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
+import {maskKey} from '../../utils/maskKey';
import PageHeader from '../../components/PageHeader/PageHeader';
import PageDescription from '../../components/PageDescription/PageDescription';
import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch';
-import SaveButton from '../../components/SaveButton/SaveButton';
+import Input from '../../components/Input/Input';
+import Checkbox from '../../components/Checkbox/Checkbox';
+import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
import styles from './WOPISettings.module.scss';
function WOPISettings() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
- const schema = useSelector(selectSchema);
- const loading = useSelector(selectConfigLoading);
const {validateField, hasValidationErrors} = useFieldValidation();
- // Local state for WOPI enable setting
+ // Local state for WOPI settings
const [localWopiEnabled, setLocalWopiEnabled] = useState(false);
+ const [localRotateKeys, setLocalRotateKeys] = useState(false);
+ const [localRefreshLockInterval, setLocalRefreshLockInterval] = useState('');
const [hasChanges, setHasChanges] = useState(false);
+ const hasInitialized = useRef(false);
- // Get the actual config value
+ // Get the actual config values
const configWopiEnabled = getNestedValue(config, 'wopi.enable', false);
+ const wopiPublicKey = getNestedValue(config, 'wopi.publicKey', '');
+ const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval', '10m');
- // Initialize local state when config loads
- useEffect(() => {
+ const resetToGlobalConfig = () => {
if (config) {
setLocalWopiEnabled(configWopiEnabled);
+ setLocalRotateKeys(false);
+ setLocalRefreshLockInterval(configRefreshLockInterval);
setHasChanges(false);
+ validateField('wopi.enable', configWopiEnabled);
+ validateField('wopi.refreshLockInterval', configRefreshLockInterval);
}
- }, [config, configWopiEnabled]);
+ };
- useEffect(() => {
- if (!config || !schema) {
- dispatch(fetchConfig());
- }
- }, [dispatch, config, schema]);
+ // Initialize settings from config when component loads (only once)
+ if (config && !hasInitialized.current) {
+ resetToGlobalConfig();
+ hasInitialized.current = true;
+ }
const handleWopiEnabledChange = enabled => {
setLocalWopiEnabled(enabled);
- setHasChanges(enabled !== configWopiEnabled);
+ // If WOPI is disabled, uncheck rotate keys
+ if (!enabled) {
+ setLocalRotateKeys(false);
+ }
+ setHasChanges(enabled !== configWopiEnabled || localRotateKeys || localRefreshLockInterval !== configRefreshLockInterval);
// Validate the boolean field
validateField('wopi.enable', enabled);
};
+ const handleRotateKeysChange = checked => {
+ setLocalRotateKeys(checked);
+ setHasChanges(localWopiEnabled !== configWopiEnabled || checked || localRefreshLockInterval !== configRefreshLockInterval);
+ };
+
+ const handleRefreshLockIntervalChange = value => {
+ setLocalRefreshLockInterval(value);
+ setHasChanges(localWopiEnabled !== configWopiEnabled || localRotateKeys || value !== configRefreshLockInterval);
+ validateField('wopi.refreshLockInterval', value);
+ };
+
const handleSave = async () => {
if (!hasChanges) return;
try {
- const updatedConfig = mergeNestedObjects([{'wopi.enable': localWopiEnabled}]);
- await dispatch(saveConfig(updatedConfig)).unwrap();
+ const enableChanged = localWopiEnabled !== configWopiEnabled;
+ const rotateRequested = localRotateKeys;
+ const refreshLockIntervalChanged = localRefreshLockInterval !== configRefreshLockInterval;
+
+ // Build config update object
+ const configUpdates = {};
+ if (enableChanged) {
+ configUpdates['wopi.enable'] = localWopiEnabled;
+ }
+ if (refreshLockIntervalChanged) {
+ configUpdates['wopi.refreshLockInterval'] = localRefreshLockInterval;
+ }
+
+ // If only rotate requested, just rotate keys
+ if (!enableChanged && !refreshLockIntervalChanged && rotateRequested) {
+ await dispatch(rotateWopiKeysAction()).unwrap();
+ }
+ // If config changes (enable or refreshLockInterval) but no rotate
+ else if ((enableChanged || refreshLockIntervalChanged) && !rotateRequested) {
+ const updatedConfig = mergeNestedObjects([configUpdates]);
+ await dispatch(saveConfig(updatedConfig)).unwrap();
+ }
+ // If both config changes and rotate requested, make two requests
+ else if ((enableChanged || refreshLockIntervalChanged) && rotateRequested) {
+ // First update the config settings
+ const updatedConfig = mergeNestedObjects([configUpdates]);
+ await dispatch(saveConfig(updatedConfig)).unwrap();
+ // Then rotate keys
+ await dispatch(rotateWopiKeysAction()).unwrap();
+ }
+
setHasChanges(false);
+ setLocalRotateKeys(false);
} catch (error) {
console.error('Failed to save WOPI settings:', error);
// Revert local state on error
setLocalWopiEnabled(configWopiEnabled);
+ setLocalRotateKeys(false);
+ setLocalRefreshLockInterval(configRefreshLockInterval);
setHasChanges(false);
}
};
- if (loading) {
- return
Loading WOPI settings...
;
- }
-
return (
-
+
WOPI SettingsConfigure WOPI (Web Application Open Platform Interface) support for document editing
@@ -74,11 +126,54 @@ function WOPISettings() {
-
-
- Save Changes
-
-
+ {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.
+