[feature] Add notifications rules; Fix bug 77096

This commit is contained in:
PauI Ostrovckij
2025-10-01 13:50:51 +03:00
parent 04729f51f3
commit 00e26e4265
5 changed files with 360 additions and 1 deletions

View File

@ -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')},

View File

@ -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 (
<div className={`${styles.notificationRules} ${styles.pageWithFixedSave}`}>
<PageHeader>Notification Rules</PageHeader>
<PageDescription>Configure email notification rules for license expiration and limit warnings</PageDescription>
<div className={styles.settingsSection}>
<div className={styles.sectionTitle}>License Expiration Warning</div>
<div className={styles.sectionDescription}>Configure email notifications when the license is about to expire</div>
<div className={styles.formRow}>
<Checkbox
label='Enable'
checked={localSettings.licenseExpirationWarningEnable || false}
onChange={value => handleFieldChange('licenseExpirationWarningEnable', value)}
error={getFieldError(CONFIG_PATHS.licenseExpirationWarningEnable)}
/>
</div>
<div className={styles.formRow}>
<Input
label='Repeat Interval:'
value={localSettings.licenseExpirationWarningRepeatInterval || ''}
onChange={value => handleFieldChange('licenseExpirationWarningRepeatInterval', value)}
placeholder='1d'
description='How often to repeat the warning (e.g., 1d, 1h, 30m)'
error={getFieldError(CONFIG_PATHS.licenseExpirationWarningRepeatInterval)}
/>
</div>
</div>
<div className={styles.settingsSection}>
<div className={styles.sectionTitle}>License Expiration Error</div>
<div className={styles.sectionDescription}>Configure email notifications when the license has expired</div>
<div className={styles.formRow}>
<Checkbox
label='Enable'
checked={localSettings.licenseExpirationErrorEnable || false}
onChange={value => handleFieldChange('licenseExpirationErrorEnable', value)}
error={getFieldError(CONFIG_PATHS.licenseExpirationErrorEnable)}
/>
</div>
<div className={styles.formRow}>
<Input
label='Repeat Interval:'
value={localSettings.licenseExpirationErrorRepeatInterval || ''}
onChange={value => handleFieldChange('licenseExpirationErrorRepeatInterval', value)}
placeholder='1d'
description='How often to repeat the error notification (e.g., 1d, 1h, 30m)'
error={getFieldError(CONFIG_PATHS.licenseExpirationErrorRepeatInterval)}
/>
</div>
</div>
<div className={styles.settingsSection}>
<div className={styles.sectionTitle}>License Limit Edit</div>
<div className={styles.sectionDescription}>Configure email notifications when the edit limit is reached</div>
<div className={styles.formRow}>
<Checkbox
label='Enable'
checked={localSettings.licenseLimitEditEnable || false}
onChange={value => handleFieldChange('licenseLimitEditEnable', value)}
error={getFieldError(CONFIG_PATHS.licenseLimitEditEnable)}
/>
</div>
<div className={styles.formRow}>
<Input
label='Repeat Interval:'
value={localSettings.licenseLimitEditRepeatInterval || ''}
onChange={value => handleFieldChange('licenseLimitEditRepeatInterval', value)}
placeholder='1h'
description='How often to repeat the limit warning (e.g., 1d, 1h, 30m)'
error={getFieldError(CONFIG_PATHS.licenseLimitEditRepeatInterval)}
/>
</div>
</div>
<div className={styles.settingsSection}>
<div className={styles.sectionTitle}>License Limit Live Viewer</div>
<div className={styles.sectionDescription}>Configure email notifications when the live viewer limit is reached</div>
<div className={styles.formRow}>
<Checkbox
label='Enable'
checked={localSettings.licenseLimitLiveViewerEnable || false}
onChange={value => handleFieldChange('licenseLimitLiveViewerEnable', value)}
error={getFieldError(CONFIG_PATHS.licenseLimitLiveViewerEnable)}
/>
</div>
<div className={styles.formRow}>
<Input
label='Repeat Interval:'
value={localSettings.licenseLimitLiveViewerRepeatInterval || ''}
onChange={value => handleFieldChange('licenseLimitLiveViewerRepeatInterval', value)}
placeholder='1h'
description='How often to repeat the limit warning (e.g., 1d, 1h, 30m)'
error={getFieldError(CONFIG_PATHS.licenseLimitLiveViewerRepeatInterval)}
/>
</div>
</div>
<FixedSaveButton onClick={handleSave} disabled={!hasChanges || hasValidationErrors()}>
Save Changes
</FixedSaveButton>
</div>
);
}
export default NotificationRules;

View File

@ -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;
}

View File

@ -3,7 +3,7 @@
}
.pageWithFixedSave {
padding-bottom: 80px;
padding-bottom: 40px;
}
.loading {

View File

@ -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"]
}
}
}
}
}
}
}
}
}
}
}