Implement UI for machines & system settings
This commit is contained in:
263
webui/src/pages/SystemSettings/SystemSettings.jsx
Normal file
263
webui/src/pages/SystemSettings/SystemSettings.jsx
Normal file
@@ -0,0 +1,263 @@
|
||||
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>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user