/****************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 *****************************************************************************/ import { ChangeEvent, FC, useCallback, useEffect, useRef, useState } from 'react'; import CheckIcon from '@mui/icons-material/Check'; import SaveIcon from '@mui/icons-material/Save'; import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Paper, Stack, Typography } from '@mui/material'; import type { Settings } from '../../extension-registry-types'; import { handleError } from '../../utils'; import { SettingsItem } from './settings-item'; import { useSettings, useUpdateSettings } from './use-settings'; interface NotificationState { id: string; message: string; severity: 'error'; timeout: ReturnType; } const NOTIFICATION_TIMEOUT = 2000; const SETTINGS: Record = { readOnly: { title: 'Read-only mode', description: 'Blocks write operations while keeping browsing, search, and downloads available.' } }; export const RuntimeSettingsPage: FC = () => { const { data: settings, isLoading: loading, error: loadError } = useSettings(); const { mutate: saveSettings, isPending: saving } = useUpdateSettings(); const [draftSettings, setDraftSettings] = useState(null); const [errorDismissed, setErrorDismissed] = useState(false); const [notifications, setNotifications] = useState([]); const [confirmOpen, setConfirmOpen] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); const saveSuccessTimer = useRef | null>(null); // Keep the editable draft in sync with the loaded (and freshly saved) settings. useEffect(() => { if (settings) { setDraftSettings(settings); } }, [settings]); // A fresh load error should be shown again even if a previous one was dismissed. useEffect(() => { setErrorDismissed(false); }, [loadError]); useEffect( () => () => { notifications.forEach(n => clearTimeout(n.timeout)); }, [] ); useEffect( () => () => { if (saveSuccessTimer.current) clearTimeout(saveSuccessTimer.current); }, [] ); const error = loadError && !errorDismissed ? handleError(loadError as Error) : null; const addNotification = useCallback((notification: Pick) => { const id = crypto.randomUUID(); const timeout = setTimeout(() => { setNotifications(current => current.filter(n => n.id !== id)); }, NOTIFICATION_TIMEOUT); setNotifications(current => [...current, { ...notification, severity: 'error', id, timeout }]); }, []); const handleNotificationClose = (id: string) => { setNotifications(current => { const notification = current.find(n => n.id === id); if (notification) clearTimeout(notification.timeout); return current.filter(n => n.id !== id); }); }; const handleFlagChange = useCallback( (key: keyof Settings) => (_event: ChangeEvent, checked: boolean) => { setDraftSettings(current => (current ? { ...current, [key]: checked } : current)); setSaveSuccess(false); }, [] ); const hasChanges = draftSettings !== null && settings != null && (Object.keys(SETTINGS) as (keyof Settings)[]).some(k => draftSettings[k] !== settings[k]); const handleSaveClick = () => setConfirmOpen(true); const handleConfirmClose = () => setConfirmOpen(false); const handleConfirmSave = useCallback(() => { if (!draftSettings) return; setConfirmOpen(false); saveSettings(draftSettings, { onSuccess: () => { setSaveSuccess(true); if (saveSuccessTimer.current) clearTimeout(saveSuccessTimer.current); saveSuccessTimer.current = setTimeout(() => setSaveSuccess(false), 2000); }, onError: err => { addNotification({ message: `Failed to save runtime settings. ${handleError(err as Error)}` }); } }); }, [draftSettings, saveSettings, addNotification]); return ( <> Settings Manage runtime settings that apply across the registry. {error && ( setErrorDismissed(true)}> {error} )} {(Object.entries(SETTINGS) as [keyof Settings, { title: string; description: string }][]).map( ([key, flag]) => ( ) )} Apply settings? These changes will be applied immediately and will affect all users of the registry. Make sure you understand the impact before proceeding. {notifications.length > 0 && ( theme.zIndex.snackbar, width: 'min(420px, calc(100vw - 32px))' }}> {notifications.map(notification => ( handleNotificationClose(notification.id)} severity={notification.severity} variant='filled' sx={{ width: '100%' }}> {notification.message} ))} )} ); };