diff --git a/AdminPanel/client/src/api/index.js b/AdminPanel/client/src/api/index.js
index b39ebe7c..43e700a5 100644
--- a/AdminPanel/client/src/api/index.js
+++ b/AdminPanel/client/src/api/index.js
@@ -41,6 +41,13 @@ export const fetchConfigurationSchema = async () => {
return response.json();
};
+export const fetchBaseConfiguration = async () => {
+ const response = await safeFetch(`${API_BASE_PATH}/config/baseconfig`, {credentials: 'include'});
+ if (response.status === 401) throw new Error('UNAUTHORIZED');
+ if (!response.ok) throw new Error('Failed to fetch base configuration');
+ return response.json();
+};
+
export const updateConfiguration = async configData => {
const response = await safeFetch(`${API_BASE_PATH}/config`, {
method: 'PATCH',
diff --git a/AdminPanel/client/src/hooks/useSchemaLoader.js b/AdminPanel/client/src/hooks/useSchemaLoader.js
index cab42587..844c236b 100644
--- a/AdminPanel/client/src/hooks/useSchemaLoader.js
+++ b/AdminPanel/client/src/hooks/useSchemaLoader.js
@@ -1,29 +1,47 @@
import {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
-import {selectSchema, selectSchemaLoading, selectSchemaError, fetchSchema} from '../store/slices/configSlice';
+import {
+ selectSchema,
+ selectSchemaLoading,
+ selectSchemaError,
+ selectBaseConfig,
+ selectBaseConfigLoading,
+ selectBaseConfigError,
+ fetchSchema,
+ fetchBaseConfig
+} from '../store/slices/configSlice';
import {selectIsAuthenticated} from '../store/slices/userSlice';
/**
- * Hook to load schema for authenticated users
- * Fetches schema immediately when the hook is first used
+ * Hook to load schema and baseConfig for authenticated users
+ * Fetches both immediately when the hook is first used
*/
export const useSchemaLoader = () => {
const dispatch = useDispatch();
const schema = useSelector(selectSchema);
const schemaLoading = useSelector(selectSchemaLoading);
const schemaError = useSelector(selectSchemaError);
+ const baseConfig = useSelector(selectBaseConfig);
+ const baseConfigLoading = useSelector(selectBaseConfigLoading);
+ const baseConfigError = useSelector(selectBaseConfigError);
const isAuthenticated = useSelector(selectIsAuthenticated);
useEffect(() => {
- // Load schema only for authenticated users
+ // Load schema and baseConfig only for authenticated users
if (isAuthenticated && !schema && !schemaLoading && !schemaError) {
dispatch(fetchSchema());
}
- }, [isAuthenticated, schema, schemaLoading, schemaError, dispatch]);
+ if (isAuthenticated && !baseConfig && !baseConfigLoading && !baseConfigError) {
+ dispatch(fetchBaseConfig());
+ }
+ }, [isAuthenticated, schema, schemaLoading, schemaError, baseConfig, baseConfigLoading, baseConfigError, dispatch]);
return {
schema,
schemaLoading,
- schemaError
+ schemaError,
+ baseConfig,
+ baseConfigLoading,
+ baseConfigError
};
};
diff --git a/AdminPanel/client/src/pages/Expiration/Expiration.js b/AdminPanel/client/src/pages/Expiration/Expiration.js
index 5f9f521f..4ff8defe 100644
--- a/AdminPanel/client/src/pages/Expiration/Expiration.js
+++ b/AdminPanel/client/src/pages/Expiration/Expiration.js
@@ -1,6 +1,6 @@
import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig} 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 Input from '../../components/Input/Input';
-import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
+import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Section from '../../components/Section/Section';
import styles from './Expiration.module.scss';
@@ -20,6 +20,7 @@ const expirationTabs = [
function Expiration() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
+ const baseConfig = useSelector(selectBaseConfig);
const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
const [activeTab, setActiveTab] = useState('garbage-collection');
@@ -46,12 +47,27 @@ function Expiration() {
sessionabsolute: 'services.CoAuthoring.expire.sessionabsolute'
};
+ const TAB_FIELDS = {
+ 'garbage-collection': ['filesCron', 'documentsCron', 'files', 'filesremovedatonce'],
+ 'session-management': ['sessionidle', 'sessionabsolute']
+ };
+
+ const computeHasChanges = (nextSettings = localSettings) => {
+ if (!config) return false;
+
+ return Object.keys(CONFIG_PATHS).some(key => {
+ const currentValue = nextSettings[key];
+ const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key]);
+ return currentValue.toString() !== originalFieldValue.toString();
+ });
+ };
+
// Reset state and errors to global config
const resetToGlobalConfig = () => {
if (config) {
const settings = {};
Object.keys(CONFIG_PATHS).forEach(key => {
- const value = getNestedValue(config, CONFIG_PATHS[key], '');
+ const value = getNestedValue(config, CONFIG_PATHS[key]);
settings[key] = value;
});
setLocalSettings(settings);
@@ -77,11 +93,6 @@ function Expiration() {
// Handle field changes
const handleFieldChange = (field, value) => {
- setLocalSettings(prev => ({
- ...prev,
- [field]: value
- }));
-
// Validate fields with schema validation
if (value !== '' && CONFIG_PATHS[field]) {
let validationValue = value;
@@ -97,14 +108,28 @@ function Expiration() {
}
}
- // Check if there are changes
- const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => {
- const currentValue = key === field ? value : localSettings[key];
- const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], '');
- return currentValue.toString() !== originalFieldValue.toString();
+ setLocalSettings(prev => {
+ const updatedSettings = {
+ ...prev,
+ [field]: value
+ };
+ setHasChanges(computeHasChanges(updatedSettings));
+ return updatedSettings;
+ });
+ };
+
+ const resetToBaseConfig = () => {
+ const fieldsToReset = TAB_FIELDS[activeTab] || [];
+ const updatedSettings = {...localSettings};
+
+ fieldsToReset.forEach(key => {
+ const value = getNestedValue(baseConfig, CONFIG_PATHS[key]);
+ updatedSettings[key] = value;
+ clearFieldError(CONFIG_PATHS[key]);
});
- setHasChanges(hasFieldChanges);
+ setLocalSettings(updatedSettings);
+ setHasChanges(computeHasChanges(updatedSettings));
};
// Handle save
@@ -225,9 +250,19 @@ function Expiration() {
{renderTabContent()}
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/pages/FileLimits/FileLimits.js b/AdminPanel/client/src/pages/FileLimits/FileLimits.js
index 6b9c7d86..be341f37 100644
--- a/AdminPanel/client/src/pages/FileLimits/FileLimits.js
+++ b/AdminPanel/client/src/pages/FileLimits/FileLimits.js
@@ -1,20 +1,21 @@
import {useState, useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig} 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 FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
+import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Section from '../../components/Section/Section';
import styles from './FileLimits.module.scss';
function FileLimits() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
- const {validateField, getFieldError, hasValidationErrors} = useFieldValidation();
+ const baseConfig = useSelector(selectBaseConfig);
+ const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
// Local state for form fields
const [localSettings, setLocalSettings] = useState({
@@ -35,16 +36,46 @@ function FileLimits() {
vsdxUncompressed: 'FileConverter.converter.inputLimits.3.zip.uncompressed'
};
+ const computeHasChanges = (nextSettings = localSettings) => {
+ if (!config) return false;
+
+ return Object.keys(nextSettings).some(key => {
+ const currentValue = nextSettings[key];
+ let originalValue;
+
+ if (key === 'maxDownloadBytes') {
+ originalValue = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes');
+ } else {
+ const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits');
+ if (key === 'docxUncompressed') {
+ const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx'));
+ originalValue = docxLimit?.zip?.uncompressed;
+ } else if (key === 'xlsxUncompressed') {
+ const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx'));
+ originalValue = xlsxLimit?.zip?.uncompressed;
+ } else if (key === 'pptxUncompressed') {
+ const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx'));
+ originalValue = pptxLimit?.zip?.uncompressed;
+ } else if (key === 'vsdxUncompressed') {
+ const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx'));
+ originalValue = vsdxLimit?.zip?.uncompressed;
+ }
+ }
+
+ return currentValue.toString() !== originalValue.toString();
+ });
+ };
+
// Load config data when component mounts
useEffect(() => {
if (config) {
const settings = {};
// Get max download bytes
- settings.maxDownloadBytes = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', '');
+ settings.maxDownloadBytes = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes');
// Get input limits - need to handle array structure
- const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []);
+ const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits');
// Find limits by document type
const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx'));
@@ -77,35 +108,38 @@ function FileLimits() {
}
}
- // Check if there are changes
- const hasFieldChanges = Object.keys(localSettings).some(key => {
- const currentValue = key === field ? value : localSettings[key];
- let originalValue;
+ setLocalSettings(prev => {
+ const updatedSettings = {
+ ...prev,
+ [field]: value
+ };
+ setHasChanges(computeHasChanges(updatedSettings));
+ return updatedSettings;
+ });
+ };
- if (key === 'maxDownloadBytes') {
- originalValue = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', '');
- } else {
- // Handle input limits array structure for comparison
- const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []);
- if (key === 'docxUncompressed') {
- const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx'));
- originalValue = docxLimit?.zip?.uncompressed || '';
- } else if (key === 'xlsxUncompressed') {
- const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx'));
- originalValue = xlsxLimit?.zip?.uncompressed || '';
- } else if (key === 'pptxUncompressed') {
- const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx'));
- originalValue = pptxLimit?.zip?.uncompressed || '';
- } else if (key === 'vsdxUncompressed') {
- const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx'));
- originalValue = vsdxLimit?.zip?.uncompressed || '';
- }
- }
+ const resetToBaseConfig = () => {
+ const settings = {};
- return currentValue.toString() !== originalValue.toString();
+ settings.maxDownloadBytes = getNestedValue(baseConfig, 'FileConverter.converter.maxDownloadBytes');
+
+ const inputLimits = getNestedValue(baseConfig, 'FileConverter.converter.inputLimits');
+ const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx'));
+ const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx'));
+ const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx'));
+ const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx'));
+
+ settings.docxUncompressed = docxLimit?.zip?.uncompressed || '';
+ settings.xlsxUncompressed = xlsxLimit?.zip?.uncompressed || '';
+ settings.pptxUncompressed = pptxLimit?.zip?.uncompressed || '';
+ settings.vsdxUncompressed = vsdxLimit?.zip?.uncompressed || '';
+
+ Object.values(CONFIG_PATHS).forEach(path => {
+ clearFieldError(path);
});
- setHasChanges(hasFieldChanges);
+ setLocalSettings(settings);
+ setHasChanges(computeHasChanges(settings));
};
// Handle save
@@ -119,7 +153,7 @@ function FileLimits() {
configUpdate['FileConverter.converter.maxDownloadBytes'] = parseInt(localSettings.maxDownloadBytes);
// Update input limits - we need to preserve the existing structure
- const currentInputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []);
+ const currentInputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits');
const updatedInputLimits = currentInputLimits.map(limit => {
if (limit.type && limit.type.includes('docx')) {
return {
@@ -229,9 +263,19 @@ function FileLimits() {
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js
index 45140bfd..039060ee 100644
--- a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js
+++ b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js
@@ -1,13 +1,13 @@
import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig} 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 Select from '../../components/Select/Select';
-import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
+import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Section from '../../components/Section/Section';
import styles from './LoggerConfig.module.scss';
@@ -25,6 +25,7 @@ const LOG_LEVELS = [
function LoggerConfig() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
+ const baseConfig = useSelector(selectBaseConfig);
const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
// Local state for form fields
@@ -39,21 +40,36 @@ function LoggerConfig() {
logLevel: 'log.options.categories.default.level'
};
+ const computeHasChanges = (nextSettings = localSettings) => {
+ if (!config) return false;
+ return Object.keys(CONFIG_PATHS).some(key => {
+ const currentValue = nextSettings[key];
+ const originalValue = getNestedValue(config, CONFIG_PATHS[key]);
+ return currentValue !== originalValue;
+ });
+ };
+
// Reset state and errors to global config
const resetToGlobalConfig = () => {
if (config) {
const settings = {
- logLevel: getNestedValue(config, CONFIG_PATHS.logLevel, 'INFO')
+ logLevel: getNestedValue(config, CONFIG_PATHS.logLevel)
};
setLocalSettings(settings);
setHasChanges(false);
- // Clear validation errors for all fields
Object.values(CONFIG_PATHS).forEach(path => {
clearFieldError(path);
});
}
};
+ const resetToBaseConfig = () => {
+ const baseValue = getNestedValue(baseConfig, CONFIG_PATHS.logLevel);
+ setLocalSettings({logLevel: baseValue});
+ clearFieldError(CONFIG_PATHS.logLevel);
+ setHasChanges(computeHasChanges({logLevel: baseValue}));
+ };
+
// Initialize settings from config when component loads (only once)
if (config && !hasInitialized.current) {
resetToGlobalConfig();
@@ -62,25 +78,18 @@ function LoggerConfig() {
// Handle field changes
const handleFieldChange = (field, value) => {
- setLocalSettings(prev => ({
- ...prev,
- [field]: value
- }));
-
- // Validate fields with schema validation
if (CONFIG_PATHS[field]) {
validateField(CONFIG_PATHS[field], value);
}
- // Check if there are changes
- const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => {
- const currentValue = key === field ? value : localSettings[key];
- const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], 'INFO');
-
- return currentValue.toString() !== originalFieldValue.toString();
+ setLocalSettings(prev => {
+ const updatedSettings = {
+ ...prev,
+ [field]: value
+ };
+ setHasChanges(computeHasChanges(updatedSettings));
+ return updatedSettings;
});
-
- setHasChanges(hasFieldChanges);
};
// Handle save
@@ -121,9 +130,19 @@ function LoggerConfig() {
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js
index 57f692ee..a3a8ca26 100644
--- a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js
+++ b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js
@@ -1,6 +1,6 @@
import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
@@ -9,7 +9,7 @@ import PageDescription from '../../components/PageDescription/PageDescription';
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 FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Section from '../../components/Section/Section';
import styles from './NotificationConfig.module.scss';
@@ -22,6 +22,7 @@ const emailConfigTabs = [
function EmailConfig() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
+ const baseConfig = useSelector(selectBaseConfig);
const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
const [activeTab, setActiveTab] = useState('notifications');
@@ -64,12 +65,44 @@ function EmailConfig() {
licenseLimitLiveViewerRepeatInterval: 'notification.rules.licenseLimitLiveViewer.policies.repeatInterval'
};
+ const TAB_FIELDS = {
+ notifications: [
+ 'licenseExpirationWarningEnable',
+ 'licenseExpirationWarningRepeatInterval',
+ 'licenseExpirationErrorEnable',
+ 'licenseExpirationErrorRepeatInterval',
+ 'licenseLimitEditEnable',
+ 'licenseLimitEditRepeatInterval',
+ 'licenseLimitLiveViewerEnable',
+ 'licenseLimitLiveViewerRepeatInterval'
+ ],
+ 'smtp-server': ['smtpHost', 'smtpPort', 'smtpUsername', 'smtpPassword'],
+ defaults: ['defaultFromEmail', 'defaultToEmail']
+ };
+
+ const computeHasChanges = (nextSettings = localSettings) => {
+ if (!config) return false;
+
+ return Object.keys(CONFIG_PATHS).some(key => {
+ const currentValue = nextSettings[key];
+ const originalValue = getNestedValue(config, CONFIG_PATHS[key]);
+
+ if (typeof originalValue === 'boolean') {
+ return currentValue !== originalValue;
+ }
+
+ const normalizedCurrent = currentValue === undefined || currentValue === null ? '' : currentValue.toString();
+ const normalizedOriginal = originalValue === undefined || originalValue === null ? '' : originalValue.toString();
+ return normalizedCurrent !== normalizedOriginal;
+ });
+ };
+
// Reset state and errors to global config
const resetToGlobalConfig = () => {
if (config) {
const settings = {};
Object.keys(CONFIG_PATHS).forEach(key => {
- const value = getNestedValue(config, CONFIG_PATHS[key], '');
+ const value = getNestedValue(config, CONFIG_PATHS[key]);
settings[key] = value;
});
setLocalSettings(settings);
@@ -95,41 +128,40 @@ function EmailConfig() {
// Handle field changes
const handleFieldChange = (field, value) => {
- setLocalSettings(prev => ({
- ...prev,
- [field]: value
- }));
-
- // Validate fields with schema validation
if (CONFIG_PATHS[field]) {
let validationValue = value;
- // Convert port to integer for validation
if (field === 'smtpPort' && value !== '') {
validationValue = parseInt(value);
if (!isNaN(validationValue)) {
validateField(CONFIG_PATHS[field], validationValue);
}
- } else if (typeof value === 'string') {
- validateField(CONFIG_PATHS[field], value);
- } else if (typeof value === 'boolean') {
+ } else if (typeof value === 'string' || typeof value === 'boolean') {
validateField(CONFIG_PATHS[field], value);
}
}
- // Check if there are changes
- const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => {
- const currentValue = key === field ? value : localSettings[key];
- const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], '');
+ setLocalSettings(prev => {
+ const updatedSettings = {
+ ...prev,
+ [field]: value
+ };
+ setHasChanges(computeHasChanges(updatedSettings));
+ return updatedSettings;
+ });
+ };
- // Handle different data types properly
- if (typeof originalFieldValue === 'boolean') {
- return currentValue !== originalFieldValue;
- }
- return currentValue.toString() !== originalFieldValue.toString();
+ const resetToBaseConfig = () => {
+ const fieldsToReset = TAB_FIELDS[activeTab];
+ const updatedSettings = {...localSettings};
+
+ fieldsToReset.forEach(key => {
+ updatedSettings[key] = getNestedValue(baseConfig, CONFIG_PATHS[key]);
+ clearFieldError(CONFIG_PATHS[key]);
});
- setHasChanges(hasFieldChanges);
+ setLocalSettings(updatedSettings);
+ setHasChanges(computeHasChanges(updatedSettings));
};
// Handle save
@@ -144,7 +176,7 @@ function EmailConfig() {
// Convert port to integer
if (key === 'smtpPort') {
- value = value ? parseInt(value) : 587;
+ value = parseInt(value);
}
configUpdate[path] = value;
@@ -340,9 +372,19 @@ function EmailConfig() {
{renderTabContent()}
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
index 3e8a4e01..78a394de 100644
--- a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
+++ b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js
@@ -1,6 +1,6 @@
import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
@@ -10,7 +10,7 @@ import Tabs from '../../components/Tabs/Tabs';
import AccessRules from '../../components/AccessRules/AccessRules';
import Section from '../../components/Section/Section';
import Checkbox from '../../components/Checkbox/Checkbox';
-import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
+import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import styles from './SecuritySettings.module.scss';
const securityTabs = [
@@ -27,6 +27,7 @@ const CONFIG_PATHS = {
function SecuritySettings() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
+ const baseConfig = useSelector(selectBaseConfig);
const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation();
const [activeTab, setActiveTab] = useState('ip-rules');
@@ -43,7 +44,7 @@ function SecuritySettings() {
const computeHasChanges = (nextSettings = localSettings, nextRules = localRules) => {
if (!config) return false;
- const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []);
+ const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules');
const normalizedOriginalRules = originalRules.map(rule => ({
address: rule.address,
allowed: !!rule.allowed
@@ -55,7 +56,7 @@ function SecuritySettings() {
const rulesChanged = JSON.stringify(normalizedOriginalRules) !== JSON.stringify(nextBackendRules);
const settingsChanged = Object.keys(CONFIG_PATHS).some(key => {
- const originalValue = getNestedValue(config, CONFIG_PATHS[key], false);
+ const originalValue = getNestedValue(config, CONFIG_PATHS[key]);
return originalValue !== nextSettings[key];
});
@@ -65,16 +66,16 @@ function SecuritySettings() {
const resetToGlobalConfig = () => {
if (!config) return;
- const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []);
- const uiRules = ipFilterRules.map(rule => ({
+ const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules');
+ const rules = ipFilterRules.map(rule => ({
type: rule.allowed ? 'Allow' : 'Deny',
value: rule.address
}));
- setLocalRules(uiRules);
+ setLocalRules(rules);
const newSettings = {};
Object.keys(CONFIG_PATHS).forEach(key => {
- newSettings[key] = getNestedValue(config, CONFIG_PATHS[key], false);
+ newSettings[key] = getNestedValue(config, CONFIG_PATHS[key]);
clearFieldError(CONFIG_PATHS[key]);
});
setLocalSettings(newSettings);
@@ -82,6 +83,27 @@ function SecuritySettings() {
setHasChanges(false);
};
+ const resetToBaseConfig = () => {
+ if (activeTab === 'ip-rules') {
+ const ipFilterRules = getNestedValue(baseConfig, 'services.CoAuthoring.ipfilter.rules');
+ const newRules = ipFilterRules.map(rule => ({
+ type: rule.allowed ? 'Allow' : 'Deny',
+ value: rule.address
+ }));
+ setLocalRules(newRules);
+ clearFieldError('services.CoAuthoring.ipfilter.rules');
+ setHasChanges(computeHasChanges(localSettings, newRules));
+ } else if (activeTab === 'request-filtering') {
+ const newSettings = {};
+ Object.keys(CONFIG_PATHS).forEach(key => {
+ newSettings[key] = getNestedValue(baseConfig, CONFIG_PATHS[key]);
+ clearFieldError(CONFIG_PATHS[key]);
+ });
+ setLocalSettings(newSettings);
+ setHasChanges(computeHasChanges(newSettings, localRules));
+ }
+ };
+
const handleTabChange = newTab => {
setActiveTab(newTab);
resetToGlobalConfig();
@@ -132,7 +154,7 @@ function SecuritySettings() {
address: rule.value,
allowed: rule.type === 'Allow'
}));
- const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []);
+ const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules');
if (JSON.stringify(originalRules) !== JSON.stringify(backendRules)) {
updates.push({
@@ -141,7 +163,7 @@ function SecuritySettings() {
}
const settingsChanged = Object.keys(CONFIG_PATHS).some(key => {
- const originalValue = getNestedValue(config, CONFIG_PATHS[key], false);
+ const originalValue = getNestedValue(config, CONFIG_PATHS[key]);
return originalValue !== localSettings[key];
});
@@ -224,9 +246,19 @@ function SecuritySettings() {
{renderTabContent()}
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
index c9ed5067..140a79cf 100644
--- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
+++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js
@@ -1,6 +1,6 @@
import {useState, useRef} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {saveConfig, selectConfig, rotateWopiKeysAction} from '../../store/slices/configSlice';
+import {saveConfig, selectConfig, selectBaseConfig, rotateWopiKeysAction} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
import {useFieldValidation} from '../../hooks/useFieldValidation';
@@ -10,7 +10,7 @@ import PageDescription from '../../components/PageDescription/PageDescription';
import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch';
import Input from '../../components/Input/Input';
import Checkbox from '../../components/Checkbox/Checkbox';
-import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
+import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup';
import Note from '../../components/Note/Note';
import Section from '../../components/Section/Section';
import styles from './WOPISettings.module.scss';
@@ -18,6 +18,7 @@ import styles from './WOPISettings.module.scss';
function WOPISettings() {
const dispatch = useDispatch();
const config = useSelector(selectConfig);
+ const baseConfig = useSelector(selectBaseConfig);
const {validateField, hasValidationErrors} = useFieldValidation();
// Local state for WOPI settings
@@ -28,9 +29,16 @@ function WOPISettings() {
const hasInitialized = useRef(false);
// 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');
+ const configWopiEnabled = getNestedValue(config, 'wopi.enable');
+ const wopiPublicKey = getNestedValue(config, 'wopi.publicKey');
+ const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval');
+
+ const computeHasChanges = ({wopiEnabled = localWopiEnabled, rotateKeys = localRotateKeys, refreshLockInterval = localRefreshLockInterval} = {}) => {
+ const enableChanged = wopiEnabled !== configWopiEnabled;
+ const refreshChanged = refreshLockInterval !== configRefreshLockInterval;
+ const rotateChanged = !!rotateKeys;
+ return enableChanged || refreshChanged || rotateChanged;
+ };
const resetToGlobalConfig = () => {
if (config) {
@@ -43,6 +51,19 @@ function WOPISettings() {
}
};
+ const resetToBaseConfig = () => {
+ const baseEnabled = getNestedValue(baseConfig, 'wopi.enable');
+ const baseRefreshInterval = getNestedValue(baseConfig, 'wopi.refreshLockInterval');
+
+ setLocalWopiEnabled(baseEnabled);
+ setLocalRotateKeys(false);
+ setLocalRefreshLockInterval(baseRefreshInterval);
+ setHasChanges(computeHasChanges({wopiEnabled: baseEnabled, rotateKeys: false, refreshLockInterval: baseRefreshInterval}));
+
+ validateField('wopi.enable', baseEnabled);
+ validateField('wopi.refreshLockInterval', baseRefreshInterval);
+ };
+
// Initialize settings from config when component loads (only once)
if (config && !hasInitialized.current) {
resetToGlobalConfig();
@@ -50,12 +71,10 @@ function WOPISettings() {
}
const handleWopiEnabledChange = enabled => {
+ const nextRotateKeys = enabled ? localRotateKeys : false;
setLocalWopiEnabled(enabled);
- // If WOPI is disabled, uncheck rotate keys
- if (!enabled) {
- setLocalRotateKeys(false);
- }
- setHasChanges(enabled !== configWopiEnabled || localRotateKeys || localRefreshLockInterval !== configRefreshLockInterval);
+ setLocalRotateKeys(nextRotateKeys);
+ setHasChanges(computeHasChanges({wopiEnabled: enabled, rotateKeys: nextRotateKeys}));
// Validate the boolean field
validateField('wopi.enable', enabled);
@@ -63,12 +82,12 @@ function WOPISettings() {
const handleRotateKeysChange = checked => {
setLocalRotateKeys(checked);
- setHasChanges(localWopiEnabled !== configWopiEnabled || checked || localRefreshLockInterval !== configRefreshLockInterval);
+ setHasChanges(computeHasChanges({rotateKeys: checked}));
};
const handleRefreshLockIntervalChange = value => {
setLocalRefreshLockInterval(value);
- setHasChanges(localWopiEnabled !== configWopiEnabled || localRotateKeys || value !== configRefreshLockInterval);
+ setHasChanges(computeHasChanges({refreshLockInterval: value}));
validateField('wopi.refreshLockInterval', value);
};
@@ -171,9 +190,19 @@ function WOPISettings() {
>
)}
-
- Save Changes
-
+
);
}
diff --git a/AdminPanel/client/src/store/slices/configSlice.js b/AdminPanel/client/src/store/slices/configSlice.js
index fae1fc26..688dc2a3 100644
--- a/AdminPanel/client/src/store/slices/configSlice.js
+++ b/AdminPanel/client/src/store/slices/configSlice.js
@@ -1,5 +1,12 @@
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit';
-import {fetchConfiguration, fetchConfigurationSchema, updateConfiguration, rotateWopiKeys, resetConfiguration} from '../../api';
+import {
+ fetchConfiguration,
+ fetchConfigurationSchema,
+ fetchBaseConfiguration,
+ updateConfiguration,
+ rotateWopiKeys,
+ resetConfiguration
+} from '../../api';
export const fetchConfig = createAsyncThunk('config/fetchConfig', async (_, {rejectWithValue}) => {
try {
@@ -19,6 +26,15 @@ export const fetchSchema = createAsyncThunk('config/fetchSchema', async (_, {rej
}
});
+export const fetchBaseConfig = createAsyncThunk('config/fetchBaseConfig', async (_, {rejectWithValue}) => {
+ try {
+ const baseConfig = await fetchBaseConfiguration();
+ return {baseConfig};
+ } catch (error) {
+ return rejectWithValue(error.message);
+ }
+});
+
export const saveConfig = createAsyncThunk('config/saveConfig', async (configData, {rejectWithValue}) => {
try {
const newConfig = await updateConfiguration(configData);
@@ -48,13 +64,16 @@ export const resetConfig = createAsyncThunk('config/resetConfig', async (paths,
const initialState = {
config: null,
+ baseConfig: null,
schema: null, // Full schema for admin panel
passwordSchema: null, // Minimal schema for Setup page password validation
loading: false,
schemaLoading: false,
+ baseConfigLoading: false,
saving: false,
error: null,
- schemaError: null
+ schemaError: null,
+ baseConfigError: null
};
const configSlice = createSlice({
@@ -109,6 +128,19 @@ const configSlice = createSlice({
state.schemaLoading = false;
state.schemaError = action.payload;
})
+ .addCase(fetchBaseConfig.pending, state => {
+ state.baseConfigLoading = true;
+ state.baseConfigError = null;
+ })
+ .addCase(fetchBaseConfig.fulfilled, (state, action) => {
+ state.baseConfigLoading = false;
+ state.baseConfig = action.payload.baseConfig;
+ state.baseConfigError = null;
+ })
+ .addCase(fetchBaseConfig.rejected, (state, action) => {
+ state.baseConfigLoading = false;
+ state.baseConfigError = action.payload;
+ })
// Save config cases
.addCase(saveConfig.pending, state => {
state.saving = true;
@@ -156,12 +188,15 @@ export const {updateLocalConfig, clearConfig, clearError, setPasswordSchema} = c
// Selectors
export const selectConfig = state => state.config.config;
+export const selectBaseConfig = state => state.config.baseConfig;
export const selectSchema = state => state.config.schema;
export const selectPasswordSchema = state => state.config.passwordSchema;
export const selectConfigLoading = state => state.config.loading;
export const selectSchemaLoading = state => state.config.schemaLoading;
+export const selectBaseConfigLoading = state => state.config.baseConfigLoading;
export const selectConfigSaving = state => state.config.saving;
export const selectConfigError = state => state.config.error;
export const selectSchemaError = state => state.config.schemaError;
+export const selectBaseConfigError = state => state.config.baseConfigError;
export default configSlice.reducer;
diff --git a/AdminPanel/server/sources/routes/config/config.service.js b/AdminPanel/server/sources/routes/config/config.service.js
index ac90c459..127eb341 100644
--- a/AdminPanel/server/sources/routes/config/config.service.js
+++ b/AdminPanel/server/sources/routes/config/config.service.js
@@ -36,6 +36,8 @@ const addFormats = require('ajv-formats');
const addErrors = require('ajv-errors');
const logger = require('../../../../../Common/sources/logger');
const tenantManager = require('../../../../../Common/sources/tenantManager');
+const moduleReloader = require('../../../../../Common/sources/moduleReloader');
+const utils = require('../../../../../Common/sources/utils');
const supersetSchema = require('../../../../../Common/config/schemas/config.schema.json');
const {deriveSchemaForScope, X_SCOPE_KEYWORD} = require('./config.schema.utils');
@@ -114,4 +116,20 @@ function getScopedConfig(ctx) {
return configCopy;
}
-module.exports = {validateScoped, getScopedConfig};
+/**
+ * Filters base configuration to include only fields defined in the appropriate schema
+ * @returns {Object} Filtered base configuration object
+ */
+function getScopedBaseConfig() {
+ const baseConfig = utils.deepMergeObjects({}, moduleReloader.getBaseConfig());
+
+ if (!baseConfig.log) {
+ baseConfig.log = {};
+ }
+ baseConfig.log.options = logger.getInitialLoggerConfig();
+
+ filterAdmin(baseConfig);
+ return baseConfig;
+}
+
+module.exports = {validateScoped, getScopedConfig, getScopedBaseConfig};
diff --git a/AdminPanel/server/sources/routes/config/router.js b/AdminPanel/server/sources/routes/config/router.js
index be510f80..e0813333 100644
--- a/AdminPanel/server/sources/routes/config/router.js
+++ b/AdminPanel/server/sources/routes/config/router.js
@@ -4,7 +4,7 @@ const express = require('express');
const bodyParser = require('body-parser');
const tenantManager = require('../../../../../Common/sources/tenantManager');
const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfigManager');
-const {getScopedConfig, validateScoped} = require('./config.service');
+const {getScopedConfig, getScopedBaseConfig, validateScoped} = require('./config.service');
const {validateJWT} = require('../../middleware/auth');
const cookieParser = require('cookie-parser');
const utils = require('../../../../../Common/sources/utils');
@@ -40,6 +40,21 @@ router.get('/schema', validateJWT, async (_req, res) => {
res.json(supersetSchema);
});
+router.get('/baseconfig', validateJWT, async (req, res) => {
+ const ctx = req.ctx;
+ try {
+ ctx.logger.info('baseconfig get start');
+ const scopedBaseConfig = getScopedBaseConfig();
+ res.setHeader('Content-Type', 'application/json');
+ res.json(scopedBaseConfig);
+ } catch (error) {
+ ctx.logger.error('Baseconfig get error: %s', error.stack);
+ res.status(500).json({error: 'Internal server error'});
+ } finally {
+ ctx.logger.info('baseconfig get end');
+ }
+});
+
router.patch('/', validateJWT, rawFileParser, async (req, res) => {
const ctx = req.ctx;
try {
diff --git a/Common/sources/logger.js b/Common/sources/logger.js
index d933e6ba..92f735ee 100644
--- a/Common/sources/logger.js
+++ b/Common/sources/logger.js
@@ -126,5 +126,8 @@ exports.shutdown = function (callback) {
};
exports.configureLogger = configureLogger;
exports.getLoggerConfig = function () {
- return curLogConfig;
+ return config.util.extendDeep({}, curLogConfig);
+};
+exports.getInitialLoggerConfig = function () {
+ return config.util.extendDeep({}, cachedLogConfig);
};