mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[feature] Add notifications rules; Fix bug 77096
This commit is contained in:
@ -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')},
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.pageWithFixedSave {
|
||||
padding-bottom: 80px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user