From 00e26e42655dc0cc60524e5d6841ef4469b0d292 Mon Sep 17 00:00:00 2001 From: PauI Ostrovckij Date: Wed, 1 Oct 2025 13:50:51 +0300 Subject: [PATCH] [feature] Add notifications rules; Fix bug 77096 --- AdminPanel/client/src/config/menuItems.js | 2 + .../NotificationRules/NotificationRules.js | 213 ++++++++++++++++++ .../NotificationRules.module.scss | 46 ++++ .../WOPISettings/WOPISettings.module.scss | 2 +- Common/config/schemas/config.schema.json | 98 ++++++++ 5 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 AdminPanel/client/src/pages/NotificationRules/NotificationRules.js create mode 100644 AdminPanel/client/src/pages/NotificationRules/NotificationRules.module.scss diff --git a/AdminPanel/client/src/config/menuItems.js b/AdminPanel/client/src/config/menuItems.js index 136ff269..4b056463 100644 --- a/AdminPanel/client/src/config/menuItems.js +++ b/AdminPanel/client/src/config/menuItems.js @@ -2,6 +2,7 @@ import WOPISettings from '../pages/WOPISettings/WOPISettings'; import Expiration from '../pages/Expiration/Expiration'; import SecuritySettings from '../pages/SecuritySettings/SecuritySettings'; import EmailConfig from '../pages/EmailConfig/EmailConfig'; +import NotificationRules from '../pages/NotificationRules/NotificationRules'; import FileLimits from '../pages/FileLimits/FileLimits'; import RequestFiltering from '../pages/RequestFiltering/RequestFiltering'; import LoggerConfig from '../pages/LoggerConfig/LoggerConfig'; @@ -21,6 +22,7 @@ export const menuItems = [ {key: 'wopi-settings', label: 'WOPI Settings', path: '/wopi-settings', component: WOPISettings}, {key: 'email-config', label: 'Email Config', path: '/email-config', component: EmailConfig}, {key: 'logger-config', label: 'Logger Config', path: '/logger-config', component: LoggerConfig}, + {key: 'notification-rules', label: 'Notification Rules', path: '/notification-rules', component: NotificationRules}, {key: 'health-check', label: 'Health Check', path: '/health-check', component: createMockComponent('Health Check')}, {key: 'config-check', label: 'Config Check', path: '/config-check', component: createMockComponent('Config Check')}, {key: 'database-info', label: 'Database Info', path: '/database-info', component: createMockComponent('Database Info')}, diff --git a/AdminPanel/client/src/pages/NotificationRules/NotificationRules.js b/AdminPanel/client/src/pages/NotificationRules/NotificationRules.js new file mode 100644 index 00000000..d062a0b0 --- /dev/null +++ b/AdminPanel/client/src/pages/NotificationRules/NotificationRules.js @@ -0,0 +1,213 @@ +import {useState, useRef, useEffect, useCallback} from 'react'; +import {useSelector, useDispatch} from 'react-redux'; +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 Checkbox from '../../components/Checkbox/Checkbox'; +import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import styles from './NotificationRules.module.scss'; + +function NotificationRules() { + const dispatch = useDispatch(); + const config = useSelector(selectConfig); + const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); + + // Local state for form fields + const [localSettings, setLocalSettings] = useState({}); + const [hasChanges, setHasChanges] = useState(false); + const hasInitialized = useRef(false); + + // Configuration paths + const CONFIG_PATHS = { + licenseExpirationWarningEnable: 'notification.rules.licenseExpirationWarning.enable', + licenseExpirationWarningRepeatInterval: 'notification.rules.licenseExpirationWarning.policies.repeatInterval', + licenseExpirationErrorEnable: 'notification.rules.licenseExpirationError.enable', + licenseExpirationErrorRepeatInterval: 'notification.rules.licenseExpirationError.policies.repeatInterval', + licenseLimitEditEnable: 'notification.rules.licenseLimitEdit.enable', + licenseLimitEditRepeatInterval: 'notification.rules.licenseLimitEdit.policies.repeatInterval', + licenseLimitLiveViewerEnable: 'notification.rules.licenseLimitLiveViewer.enable', + licenseLimitLiveViewerRepeatInterval: 'notification.rules.licenseLimitLiveViewer.policies.repeatInterval' + }; + + // Reset state and errors to global config + const resetToGlobalConfig = useCallback(() => { + if (config) { + const settings = {}; + Object.keys(CONFIG_PATHS).forEach(key => { + const value = getNestedValue(config, CONFIG_PATHS[key], ''); + settings[key] = value; + }); + setLocalSettings(settings); + setHasChanges(false); + // Clear validation errors for all fields + Object.values(CONFIG_PATHS).forEach(path => { + clearFieldError(path); + }); + } + }, [config, clearFieldError]); + + // Initialize settings from config when component loads + useEffect(() => { + if (config && !hasInitialized.current) { + resetToGlobalConfig(); + hasInitialized.current = true; + } + }, [config, resetToGlobalConfig]); + + // Handle field changes + const handleFieldChange = (field, value) => { + setLocalSettings(prev => ({ + ...prev, + [field]: value + })); + + // Validate fields with schema validation + if (CONFIG_PATHS[field]) { + if (typeof value === 'string') { + validateField(CONFIG_PATHS[field], value); + } else if (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], ''); + + // Handle different data types properly + if (typeof originalFieldValue === 'boolean') { + return currentValue !== originalFieldValue; + } + return currentValue.toString() !== originalFieldValue.toString(); + }); + + setHasChanges(hasFieldChanges); + }; + + // Handle save + const handleSave = async () => { + if (!hasChanges) return; + + // Create config update object + const configUpdate = {}; + Object.keys(CONFIG_PATHS).forEach(key => { + const path = CONFIG_PATHS[key]; + const value = localSettings[key]; + configUpdate[path] = value; + }); + + const mergedConfig = mergeNestedObjects([configUpdate]); + await dispatch(saveConfig(mergedConfig)).unwrap(); + setHasChanges(false); + }; + + return ( +
+ Notification Rules + Configure email notification rules for license expiration and limit warnings + +
+
License Expiration Warning
+
Configure email notifications when the license is about to expire
+
+ handleFieldChange('licenseExpirationWarningEnable', value)} + error={getFieldError(CONFIG_PATHS.licenseExpirationWarningEnable)} + /> +
+
+ handleFieldChange('licenseExpirationWarningRepeatInterval', value)} + placeholder='1d' + description='How often to repeat the warning (e.g., 1d, 1h, 30m)' + error={getFieldError(CONFIG_PATHS.licenseExpirationWarningRepeatInterval)} + /> +
+
+ +
+
License Expiration Error
+
Configure email notifications when the license has expired
+
+ handleFieldChange('licenseExpirationErrorEnable', value)} + error={getFieldError(CONFIG_PATHS.licenseExpirationErrorEnable)} + /> +
+
+ handleFieldChange('licenseExpirationErrorRepeatInterval', value)} + placeholder='1d' + description='How often to repeat the error notification (e.g., 1d, 1h, 30m)' + error={getFieldError(CONFIG_PATHS.licenseExpirationErrorRepeatInterval)} + /> +
+
+ +
+
License Limit Edit
+
Configure email notifications when the edit limit is reached
+
+ handleFieldChange('licenseLimitEditEnable', value)} + error={getFieldError(CONFIG_PATHS.licenseLimitEditEnable)} + /> +
+
+ handleFieldChange('licenseLimitEditRepeatInterval', value)} + placeholder='1h' + description='How often to repeat the limit warning (e.g., 1d, 1h, 30m)' + error={getFieldError(CONFIG_PATHS.licenseLimitEditRepeatInterval)} + /> +
+
+ +
+
License Limit Live Viewer
+
Configure email notifications when the live viewer limit is reached
+
+ handleFieldChange('licenseLimitLiveViewerEnable', value)} + error={getFieldError(CONFIG_PATHS.licenseLimitLiveViewerEnable)} + /> +
+
+ handleFieldChange('licenseLimitLiveViewerRepeatInterval', value)} + placeholder='1h' + description='How often to repeat the limit warning (e.g., 1d, 1h, 30m)' + error={getFieldError(CONFIG_PATHS.licenseLimitLiveViewerRepeatInterval)} + /> +
+
+ + + Save Changes + +
+ ); +} + +export default NotificationRules; diff --git a/AdminPanel/client/src/pages/NotificationRules/NotificationRules.module.scss b/AdminPanel/client/src/pages/NotificationRules/NotificationRules.module.scss new file mode 100644 index 00000000..026cf968 --- /dev/null +++ b/AdminPanel/client/src/pages/NotificationRules/NotificationRules.module.scss @@ -0,0 +1,46 @@ +.notificationRules { + padding: 0; +} + +.pageWithFixedSave { + padding-bottom: 40px; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + font-family: 'Open Sans', sans-serif; + color: #666666; +} + +.settingsSection { + background: white; + border: 1px solid #e2e2e2; + border-radius: 8px; + padding: 32px; + margin-bottom: 32px; +} + +.sectionTitle { + font-size: 18px; + font-weight: 600; + color: #333333; + margin-bottom: 8px; +} + +.sectionDescription { + font-size: 14px; + color: #666666; + margin-bottom: 24px; + line-height: 1.5; +} + +.formRow { + margin-bottom: 16px; + display: flex; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; +} diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss index 20f12b08..48f5c6b2 100644 --- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss +++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.module.scss @@ -3,7 +3,7 @@ } .pageWithFixedSave { - padding-bottom: 80px; + padding-bottom: 40px; } .loading { diff --git a/Common/config/schemas/config.schema.json b/Common/config/schemas/config.schema.json index 5c7c7879..fa8c2b40 100644 --- a/Common/config/schemas/config.schema.json +++ b/Common/config/schemas/config.schema.json @@ -244,6 +244,104 @@ } } } + }, + "notification": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "rules": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "licenseExpirationWarning": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]}, + "policies": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "repeatInterval": { + "type": "string", + "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$", + "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')", + "x-scope": ["admin", "tenant"] + } + } + } + } + }, + "licenseExpirationError": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]}, + "policies": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "repeatInterval": { + "type": "string", + "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$", + "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')", + "x-scope": ["admin", "tenant"] + } + } + } + } + }, + "licenseLimitEdit": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]}, + "policies": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "repeatInterval": { + "type": "string", + "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$", + "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')", + "x-scope": ["admin", "tenant"] + } + } + } + } + }, + "licenseLimitLiveViewer": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]}, + "policies": { + "type": "object", + "additionalProperties": false, + "x-scope": ["admin", "tenant"], + "properties": { + "repeatInterval": { + "type": "string", + "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$", + "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')", + "x-scope": ["admin", "tenant"] + } + } + } + } + } + } + } + } } } }