Files
Arkendro/webui/src/pages/SystemSettings/SystemSettings.jsx

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