263 lines
9.2 KiB
JavaScript
263 lines
9.2 KiB
JavaScript
import React, {useState, useEffect, useContext} from 'react';
|
|
import {UserContext} from '@/common/contexts/UserContext.jsx';
|
|
import {useToast} from '@/common/contexts/ToastContext.jsx';
|
|
import {getRequest, postRequest} from '@/common/utils/RequestUtil.js';
|
|
import Button from '@/common/components/Button';
|
|
import Input from '@/common/components/Input';
|
|
import Card, {CardHeader, CardBody} from '@/common/components/Card';
|
|
import LoadingSpinner from '@/common/components/LoadingSpinner';
|
|
import PageHeader from '@/common/components/PageHeader';
|
|
import {
|
|
SlidersIcon,
|
|
FloppyDiskIcon,
|
|
ArrowClockwiseIcon,
|
|
InfoIcon,
|
|
WarningIcon
|
|
} from '@phosphor-icons/react';
|
|
import './styles.sass';
|
|
|
|
export const SystemSettings = () => {
|
|
const toast = useToast();
|
|
const [configs, setConfigs] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [formData, setFormData] = useState({});
|
|
const [formErrors, setFormErrors] = useState({});
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchConfigs();
|
|
}, []);
|
|
|
|
const fetchConfigs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await getRequest('admin/config');
|
|
setConfigs(response.configs);
|
|
|
|
const initialData = {};
|
|
response.configs.forEach(config => {
|
|
initialData[config.key] = config.value || config.default_value || '';
|
|
});
|
|
setFormData(initialData);
|
|
setHasChanges(false);
|
|
} catch (error) {
|
|
console.error('Failed to fetch configs:', error);
|
|
toast.error('Failed to load system settings. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (key, value) => {
|
|
setFormData(prev => {
|
|
const newData = {...prev, [key]: value};
|
|
|
|
const originalConfig = configs.find(c => c.key === key);
|
|
const hasChange = Object.keys(newData).some(k => {
|
|
const original = configs.find(c => c.key === k);
|
|
const origVal = original?.value || original?.default_value || '';
|
|
return newData[k] !== origVal;
|
|
});
|
|
|
|
setHasChanges(hasChange);
|
|
return newData;
|
|
});
|
|
|
|
if (formErrors[key]) {
|
|
setFormErrors(prev => ({
|
|
...prev,
|
|
[key]: ''
|
|
}));
|
|
}
|
|
};
|
|
|
|
const validateForm = () => {
|
|
const errors = {};
|
|
|
|
configs.forEach(config => {
|
|
const value = formData[config.key] || '';
|
|
|
|
if (config.required && !value.trim()) {
|
|
errors[config.key] = `${config.key} is required`;
|
|
return;
|
|
}
|
|
|
|
switch (config.key) {
|
|
case 'EXTERNAL_URL':
|
|
if (value && !value.startsWith('http://') && !value.startsWith('https://')) {
|
|
errors[config.key] = 'URL must start with http:// or https://';
|
|
}
|
|
break;
|
|
case 'MAX_UPLOAD_SIZE_MB':
|
|
case 'BACKUP_RETENTION_DAYS':
|
|
case 'SESSION_TIMEOUT_HOURS':
|
|
if (value && (isNaN(value) || parseInt(value) <= 0)) {
|
|
errors[config.key] = 'Must be a positive number';
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
setFormErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
|
|
try {
|
|
const savePromises = [];
|
|
|
|
for (const config of configs) {
|
|
const newValue = formData[config.key] || '';
|
|
const originalValue = config.value || config.default_value || '';
|
|
|
|
if (newValue !== originalValue) {
|
|
savePromises.push(
|
|
postRequest('admin/config', {
|
|
key: config.key,
|
|
value: newValue
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
await Promise.all(savePromises);
|
|
|
|
toast.success('System settings saved successfully');
|
|
setHasChanges(false);
|
|
await fetchConfigs();
|
|
} catch (error) {
|
|
console.error('Failed to save settings:', error);
|
|
const errorMessage = error.error || 'Failed to save settings. Please try again.';
|
|
toast.error(errorMessage);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleReset = async () => {
|
|
if (!window.confirm('Are you sure you want to reset all settings to their current saved values? Any unsaved changes will be lost.')) {
|
|
return;
|
|
}
|
|
|
|
await fetchConfigs();
|
|
toast.info('Settings reset to saved values');
|
|
};
|
|
|
|
const getFieldType = (key) => {
|
|
switch (key) {
|
|
case 'SESSION_TIMEOUT_HOURS':
|
|
return 'number';
|
|
case 'EXTERNAL_URL':
|
|
return 'url';
|
|
default:
|
|
return 'text';
|
|
}
|
|
};
|
|
|
|
const getFieldPlaceholder = (config) => {
|
|
if (config.default_value) {
|
|
return `Default: ${config.default_value}`;
|
|
}
|
|
return `Enter ${config.key.toLowerCase().replace(/_/g, ' ')}`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="content">
|
|
<LoadingSpinner centered text="Loading system settings..."/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="content">
|
|
<PageHeader
|
|
title="System Settings"
|
|
subtitle="Configure system-wide settings and preferences"
|
|
actions={
|
|
<div className="settings-actions">
|
|
<Button
|
|
variant="subtle"
|
|
icon={<ArrowClockwiseIcon size={16}/>}
|
|
onClick={handleReset}
|
|
disabled={submitting || !hasChanges}
|
|
>
|
|
Reset
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
icon={<FloppyDiskIcon size={16}/>}
|
|
onClick={handleSave}
|
|
loading={submitting}
|
|
disabled={submitting || !hasChanges}
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{hasChanges && (
|
|
<div className="settings-notice">
|
|
<WarningIcon size={16}/>
|
|
<span>You have unsaved changes</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="settings-grid">
|
|
{configs.map(config => (
|
|
<Card key={config.key} className="setting-card">
|
|
<CardHeader>
|
|
<div className="setting-header">
|
|
<div className="setting-info">
|
|
<h3 className="setting-title">
|
|
{config.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
{config.required && <span className="required">*</span>}
|
|
</h3>
|
|
<p className="setting-description">{config.description}</p>
|
|
</div>
|
|
{config.default_value && (
|
|
<div className="setting-default">
|
|
<InfoIcon size={14}/>
|
|
<span>Default: {config.default_value}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardBody>
|
|
<Input
|
|
type={getFieldType(config.key)}
|
|
value={formData[config.key] || ''}
|
|
onChange={(e) => handleInputChange(config.key, e.target.value)}
|
|
placeholder={getFieldPlaceholder(config)}
|
|
error={formErrors[config.key]}
|
|
disabled={submitting}
|
|
className="setting-input"
|
|
/>
|
|
</CardBody>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{configs.length === 0 && (
|
|
<Card>
|
|
<CardBody>
|
|
<div className="empty-settings">
|
|
<SlidersIcon size={48} weight="duotone"/>
|
|
<h3>No settings available</h3>
|
|
<p>No configurable settings are currently defined.</p>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}; |