in src/application/ui/src/components/RuleManager.js [58:236]
export default function RuleManager() {
const [rules, setRules] = useState([]);
const [loading, setLoading] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
const [editingRule, setEditingRule] = useState(null);
const [error, setError] = useState(null);
const [apiHealth, setApiHealth] = useState(null);
const [currentRule, setCurrentRule] = useState({
type: RULE_TYPES.BOOST,
conditionType: CONDITION_TYPES.CATEGORY,
condition: '',
score: 1.0,
});
const checkApiHealth = async () => {
try {
const health = await ruleService.checkHealth();
setApiHealth(health);
setError(null);
} catch (error) {
setApiHealth(null);
setError(`API Connection Error: Mock rule service unavailable`);
}
};
const fetchRules = async () => {
setLoading(true);
try {
const rulesList = await ruleService.getRules();
setRules(rulesList || []);
setError(null);
} catch (error) {
setError('Failed to fetch mock rules. Please try again.');
console.error('Error fetching rules:', error);
setRules([]);
}
setLoading(false);
};
useEffect(() => {
checkApiHealth();
fetchRules();
}, []);
const handleOpenDialog = (rule = null) => {
if (rule) {
setEditingRule(rule);
setCurrentRule(rule);
} else {
setEditingRule(null);
setCurrentRule({
type: RULE_TYPES.BOOST,
conditionType: CONDITION_TYPES.CATEGORY,
condition: '',
score: 1.0,
});
}
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
setEditingRule(null);
setError(null);
};
const validateRule = (rule) => {
if (!rule.condition.trim()) {
throw new Error('Condition cannot be empty');
}
if (rule.conditionType === CONDITION_TYPES.PRICE_RANGE) {
try {
const [min, max] = rule.condition.split('-').map(Number);
if (isNaN(min) || isNaN(max) || min >= max) {
throw new Error();
}
} catch {
throw new Error('Price range must be in format min-max (e.g., 0-100)');
}
}
if (!rule.score || rule.score <= 0) {
throw new Error('Score must be greater than 0');
}
};
const handleSaveRule = async () => {
setLoading(true);
try {
validateRule(currentRule);
if (editingRule) {
await ruleService.updateRule(editingRule.id, currentRule);
} else {
await ruleService.createRule(currentRule);
}
await fetchRules();
handleCloseDialog();
setError(null);
} catch (error) {
setError(error.message || 'Failed to save mock rule. Please try again.');
console.error('Error saving rule:', error);
}
setLoading(false);
};
const handleDeleteRule = async (ruleId) => {
if (window.confirm('Are you sure you want to delete this rule?')) {
setLoading(true);
try {
await ruleService.deleteRule(ruleId);
await fetchRules();
setError(null);
} catch (error) {
setError('Failed to delete mock rule. Please try again.');
console.error('Error deleting rule:', error);
}
setLoading(false);
}
};
if (loading && rules.length === 0) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h5" sx={{ mb: 1 }}>Search Rules Management</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexDirection: 'column', alignItems: 'flex-start' }}>
{apiHealth && (
<Chip
label={`Status: ${apiHealth.status}`}
color={apiHealth.status === 'healthy' ? 'success' : 'error'}
size="small"
/>
)}
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Using mock data (Spanner implementation coming soon)
</Typography>
</Box>
</Box>
<Button
variant="contained"
onClick={() => handleOpenDialog()}
disabled={loading || !apiHealth}
>
Add New Rule
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Grid container spacing={3}>
{rules.map((rule) => (
<Grid item xs={12} md={6} key={rule.id}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography
variant="h6"
color={rule.type === RULE_TYPES.BOOST ? 'success.main' : 'error.main'}
>
{(rule.type || '').toUpperCase()}
</Typography>
<Box>
<IconButton onClick={() => handleOpenDialog(rule)}>
<EditIcon />
</IconButton>
<IconButton onClick={() => handleDeleteRule(rule.id)}>